diff --git a/changelog.d/5-internal/wpb-6329_ejpd_stuff b/changelog.d/5-internal/wpb-6329_ejpd_stuff new file mode 100644 index 0000000000..6da6f5b419 --- /dev/null +++ b/changelog.d/5-internal/wpb-6329_ejpd_stuff @@ -0,0 +1,6 @@ +Add assets to output of ejpd-info end-point in stern; also: + +- [brig] now talks to carghold for profile picture extraction; +- [integration] migrate ejpd tests; +- [integration] enhanced `shouldMatch` shows a diff on failure now; +- [integration] added `shouldMatchLeniently` for rule-based canonicalization of arguments diff --git a/integration/default.nix b/integration/default.nix index 36f503e3c9..a259708844 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -4,6 +4,7 @@ # dependencies are added or removed. { mkDerivation , aeson +, aeson-diff , aeson-pretty , array , async @@ -72,6 +73,7 @@ , warp-tls , websockets , wire-message-proto-lens +, wreq , xml , yaml }: @@ -91,6 +93,7 @@ mkDerivation { ]; libraryHaskellDepends = [ aeson + aeson-diff aeson-pretty array async @@ -155,6 +158,7 @@ mkDerivation { warp-tls websockets wire-message-proto-lens + wreq xml yaml ]; diff --git a/integration/integration.cabal b/integration/integration.cabal index 1a447c9b7f..bcafb9ff14 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -118,6 +118,7 @@ library Test.Connection Test.Conversation Test.Demo + Test.EJPD Test.Errors Test.ExternalPartner Test.FeatureFlags @@ -166,6 +167,7 @@ library build-depends: , aeson + , aeson-diff , aeson-pretty , array , async @@ -230,5 +232,6 @@ library , warp-tls , websockets , wire-message-proto-lens + , wreq , xml , yaml diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index 5eef85edea..d538bb3556 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -243,3 +243,13 @@ getClientsFull user users = do val <- make users baseRequest user Brig Unversioned do joinHttpPath ["i", "clients", "full"] >>= submit "POST" . addJSONObject ["users" .= val] + +-- | https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/post_i_ejpd_request +getEJPDInfo :: (HasCallStack, MakesValue dom) => dom -> [String] -> String -> App Response +getEJPDInfo dom handles mode = do + req <- rawBaseRequest dom Brig Unversioned "/i/ejpd-request" + let query = case mode of + "" -> [] + "include_contacts" -> [("include_contacts", "true")] + bad -> error $ show bad + submit "POST" $ req & addJSONObject ["ejpd_request" .= handles] & addQueryParams query diff --git a/integration/test/API/Cargohold.hs b/integration/test/API/Cargohold.hs index 595ce75327..0fe767fea3 100644 --- a/integration/test/API/Cargohold.hs +++ b/integration/test/API/Cargohold.hs @@ -37,7 +37,10 @@ uploadAssetV3 user isPublic retention mimeType bdy = do multipartMixedMime = "multipart/mixed; boundary=" <> multipartBoundary uploadAsset :: (HasCallStack, MakesValue user) => user -> App Response -uploadAsset user = do +uploadAsset = flip uploadFreshAsset "Hello World!" + +uploadFreshAsset :: (HasCallStack, MakesValue user) => user -> String -> App Response +uploadFreshAsset user payload = do uid <- user & objId req <- baseRequest user Cargohold Versioned "/assets" bdy <- txtAsset @@ -51,7 +54,7 @@ uploadAsset user = do buildUploadAssetRequestBody True (Nothing :: Maybe String) - (LBSC.pack "Hello World!") + (LBSC.pack payload) textPlainMime textPlainMime :: MIME.MIMEType @@ -104,13 +107,25 @@ instance MakesValue loc => IsAssetLocation loc where noRedirect :: Request -> Request noRedirect r = r {redirectCount = 0} -downloadAsset' :: (HasCallStack, MakesValue user, IsAssetLocation loc, IsAssetToken tok) => user -> loc -> tok -> App Response +downloadAsset' :: + (HasCallStack, MakesValue user, IsAssetLocation loc, IsAssetToken tok) => + user -> + loc -> + tok -> + App Response downloadAsset' user loc tok = do locPath <- locationPathFragment loc req <- baseRequest user Cargohold Unversioned $ locPath submit "GET" $ req & tokenParam tok & noRedirect -downloadAsset :: (HasCallStack, MakesValue user, MakesValue key, MakesValue assetDomain) => user -> assetDomain -> key -> String -> (HTTP.Request -> HTTP.Request) -> App Response +downloadAsset :: + (HasCallStack, MakesValue user, MakesValue key, MakesValue assetDomain) => + user -> + assetDomain -> + key -> + String -> + (HTTP.Request -> HTTP.Request) -> + App Response downloadAsset user assetDomain key zHostHeader trans = do domain <- objDomain assetDomain key' <- asString key diff --git a/integration/test/API/Common.hs b/integration/test/API/Common.hs index 623a2c5657..066c360a42 100644 --- a/integration/test/API/Common.hs +++ b/integration/test/API/Common.hs @@ -14,8 +14,10 @@ teamRole "admin" = 5951 teamRole "owner" = 8191 teamRole bad = error $ "unknown team role: " <> bad +-- | please don't use special shell characters like '!' here. it makes writing shell lines +-- that use test data a lot less straight-forward. defPassword :: String -defPassword = "hunter2!" +defPassword = "hunter2." randomEmail :: App String randomEmail = do diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 0c263d969e..0181d9325c 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -5,6 +5,7 @@ module SetupHelpers where import API.Brig import API.BrigInternal +import API.Cargohold import API.Common import API.Galley import API.GalleyInternal (legalholdWhitelistTeam) @@ -14,8 +15,10 @@ import Data.Aeson hiding ((.=)) import qualified Data.Aeson.Types as Aeson import qualified Data.ByteString.Base64.URL as B64Url import Data.ByteString.Char8 (unpack) +import qualified Data.CaseInsensitive as CI import Data.Default import Data.Function +import Data.String.Conversions (cs) import Data.UUID.V1 (nextUUID) import Data.UUID.V4 (nextRandom) import GHC.Stack @@ -90,7 +93,7 @@ connectTwoUsers alice bob = do bindResponse (postConnection alice bob) (\resp -> resp.status `shouldMatchInt` 201) bindResponse (putConnection bob alice "accepted") (\resp -> resp.status `shouldMatchInt` 200) -connectUsers :: HasCallStack => [Value] -> App () +connectUsers :: (HasCallStack, MakesValue usr) => [usr] -> App () connectUsers users = traverse_ (uncurry connectTwoUsers) $ do t <- tails users (a, others) <- maybeToList (uncons t) @@ -326,3 +329,45 @@ randomScimUser = do "userName" .= handle, "displayName" .= handle ] + +-- | This adds one random asset to the `assets` field in the user record and returns an asset +-- key. The asset carries a fresh UUIDv4 in text form (even though it is typed 'preview` and +-- `image'). +uploadProfilePicture :: (HasCallStack, MakesValue usr) => usr -> App (String, String, String) +uploadProfilePicture usr = do + payload <- ("asset_contents=" <>) <$> randomId + asset <- bindResponse (uploadFreshAsset usr payload) (getJSON 201) + dom <- asset %. "domain" & asString + key <- asset %. "key" & asString + Success (oldAssets :: [Value]) <- bindResponse (getSelf usr) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "assets" <&> fromJSON + bindResponse + (putSelf usr def {assets = Just (object ["key" .= key, "size" .= "preview", "type" .= "image"] : oldAssets)}) + assertSuccess + pure (dom, key, payload) + +-- | Take a calling user (any user will do) and an asset domain and key, and return a +-- (temporarily valid) s3 url plus asset payload (if created with `uploadProfilePicture`, +-- that's a UUIDv4). +downloadProfilePicture :: (HasCallStack, MakesValue caller) => caller -> String -> String -> App (String, String) +downloadProfilePicture caller assetDomain assetKey = do + locurl <- bindResponse (downloadAsset caller caller assetKey assetDomain noRedirect) $ \resp -> do + resp.status `shouldMatchInt` 302 + maybe + (error "no location header in 302 response!?") + (pure . cs) + (lookup (CI.mk (cs "Location")) resp.headers) + + payload <- bindResponse (downloadAsset caller caller assetKey assetDomain id) $ \resp -> do + resp.status `shouldMatchInt` 200 + pure $ cs resp.body + + pure (locurl, payload) + +-- | Call 'uploadProfilePicture' and 'downloadPicture', returning the return value of the +-- latter. +uploadDownloadProfilePicture :: (HasCallStack, MakesValue usr) => usr -> App (String, String) +uploadDownloadProfilePicture usr = do + (dom, key, _payload) <- uploadProfilePicture usr + downloadProfilePicture usr dom key diff --git a/integration/test/Test/AssetDownload.hs b/integration/test/Test/AssetDownload.hs index 2d73fb7ff9..68b60c8545 100644 --- a/integration/test/Test/AssetDownload.hs +++ b/integration/test/Test/AssetDownload.hs @@ -2,8 +2,6 @@ module Test.AssetDownload where import API.Cargohold import GHC.Stack -import Network.HTTP.Client (Request (redirectCount)) -import qualified Network.HTTP.Client as HTTP import SetupHelpers import Testlib.Prelude @@ -28,16 +26,16 @@ testDownloadAssetMultiIngressS3DownloadUrl = do -- multi-ingress disabled key <- doUploadAsset user - bindResponse (downloadAsset user user key "nginz-https.example.com" noRedirects) $ \resp -> do + bindResponse (downloadAsset user user key "nginz-https.example.com" noRedirect) $ \resp -> do resp.status `shouldMatchInt` 302 - bindResponse (downloadAsset user user key "red.example.com" noRedirects) $ \resp -> do + bindResponse (downloadAsset user user key "red.example.com" noRedirect) $ \resp -> do resp.status `shouldMatchInt` 302 - bindResponse (downloadAsset user user key "green.example.com" noRedirects) $ \resp -> do + bindResponse (downloadAsset user user key "green.example.com" noRedirect) $ \resp -> do resp.status `shouldMatchInt` 302 - bindResponse (downloadAsset user user key "unknown.example.com" noRedirects) $ \resp -> do + bindResponse (downloadAsset user user key "unknown.example.com" noRedirect) $ \resp -> do resp.status `shouldMatchInt` 302 -- multi-ingress enabled @@ -45,25 +43,22 @@ testDownloadAssetMultiIngressS3DownloadUrl = do user' <- randomUser domain def key' <- doUploadAsset user' - bindResponse (downloadAsset user' user' key' "nginz-https.example.com" noRedirects) $ \resp -> do + bindResponse (downloadAsset user' user' key' "nginz-https.example.com" noRedirect) $ \resp -> do resp.status `shouldMatchInt` 404 resp.json %. "label" `shouldMatch` "not-found" - bindResponse (downloadAsset user' user' key' "red.example.com" noRedirects) $ \resp -> do + bindResponse (downloadAsset user' user' key' "red.example.com" noRedirect) $ \resp -> do resp.status `shouldMatchInt` 302 locationHeaderHost resp `shouldMatch` "s3-download.red.example.com" - bindResponse (downloadAsset user' user' key' "green.example.com" noRedirects) $ \resp -> do + bindResponse (downloadAsset user' user' key' "green.example.com" noRedirect) $ \resp -> do resp.status `shouldMatchInt` 302 locationHeaderHost resp `shouldMatch` "s3-download.green.example.com" - bindResponse (downloadAsset user' user' key' "unknown.example.com" noRedirects) $ \resp -> do + bindResponse (downloadAsset user' user' key' "unknown.example.com" noRedirect) $ \resp -> do resp.status `shouldMatchInt` 404 resp.json %. "label" `shouldMatch` "not-found" where - noRedirects :: HTTP.Request -> HTTP.Request - noRedirects req = (req {redirectCount = 0}) - modifyConfig :: ServiceOverrides modifyConfig = def diff --git a/integration/test/Test/EJPD.hs b/integration/test/Test/EJPD.hs new file mode 100644 index 0000000000..b20e74a663 --- /dev/null +++ b/integration/test/Test/EJPD.hs @@ -0,0 +1,172 @@ +{-# OPTIONS -Wno-ambiguous-fields #-} +module Test.EJPD (testEJPDRequest) where + +import API.Brig +import qualified API.BrigInternal as BI +import API.Gundeck +import Control.Lens hiding ((.=)) +import Control.Monad.Reader +import qualified Data.Aeson as A +import Data.Aeson.Lens +import Data.String.Conversions (cs) +import qualified Data.UUID as UUID +import qualified Data.UUID.V4 as UUID +import qualified Network.Wreq as Wreq +import SetupHelpers +import Testlib.JSON +import Testlib.Prelude + +-- | Create some teams & users, and return their expected ejpd response values. +setupEJPD :: HasCallStack => App (A.Value, A.Value, A.Value, A.Value, A.Value) +setupEJPD = + do + (owner1, _tid1, [usr1, usr2]) <- createTeam OwnDomain 3 + handle1 <- liftIO $ UUID.nextRandom <&> ("usr1-handle-" <>) . UUID.toString + handle2 <- liftIO $ UUID.nextRandom <&> ("usr2-handle-" <>) . UUID.toString + void $ putHandle usr1 handle1 + void $ putHandle usr2 handle2 + email3 <- liftIO $ UUID.nextRandom <&> \uuid -> "usr3-" <> UUID.toString uuid <> "@example.com" + email4 <- liftIO $ UUID.nextRandom <&> \uuid -> "usr4-" <> UUID.toString uuid <> "@example.com" + email5 <- liftIO $ UUID.nextRandom <&> \uuid -> "usr5-" <> UUID.toString uuid <> "@example.com" + usr3 <- randomUser OwnDomain def {BI.email = Just email3, BI.name = Just "usr3"} + usr4 <- randomUser OwnDomain def {BI.email = Just email4, BI.name = Just "usr4"} + usr5 <- randomUser OwnDomain def {BI.email = Just email5, BI.name = Just "usr5"} + handle3 <- liftIO $ UUID.nextRandom <&> ("usr3-handle-" <>) . UUID.toString + handle4 <- liftIO $ UUID.nextRandom <&> ("usr4-handle-" <>) . UUID.toString + handle5 <- liftIO $ UUID.nextRandom <&> ("usr5-handle-" <>) . UUID.toString + void $ putHandle usr3 handle3 + void $ putHandle usr4 handle4 + void $ putHandle usr5 handle5 + + connectTwoUsers usr3 usr5 + connectTwoUsers usr2 usr4 + connectTwoUsers usr4 usr5 + + toks1 <- do + cl11 <- objId $ addClient (usr1 %. "qualified_id") def >>= getJSON 201 + bindResponse (postPushToken usr1 cl11 def) $ \resp -> do + resp.status `shouldMatchInt` 201 + tok <- resp.json %. "token" & asString + pure [tok] + toks2 <- do + cl21 <- objId $ addClient (usr2 %. "qualified_id") def >>= getJSON 201 + cl22 <- objId $ addClient (usr2 %. "qualified_id") def >>= getJSON 201 + t1 <- bindResponse (postPushToken usr2 cl21 def) $ \resp -> do + resp.status `shouldMatchInt` 201 + resp.json %. "token" & asString + t2 <- bindResponse (postPushToken usr2 cl22 def) $ \resp -> do + resp.status `shouldMatchInt` 201 + resp.json %. "token" & asString + pure [t1, t2] + toks4 <- do + cl41 <- objId $ addClient (usr4 %. "qualified_id") def >>= getJSON 201 + bindResponse (postPushToken usr4 cl41 def) $ \resp -> do + resp.status `shouldMatchInt` 201 + tok <- resp.json %. "token" & asString + pure [tok] + + assets1 <- do + a1 <- uploadDownloadProfilePicture usr1 + a2 <- uploadDownloadProfilePicture usr1 + pure $ snd <$> [a1, a2] + assets2 <- do + (: []) . snd <$> uploadDownloadProfilePicture usr2 + assets3 <- do + (: []) . snd <$> uploadDownloadProfilePicture usr3 + assets4 <- do + (: []) . snd <$> uploadDownloadProfilePicture usr4 + + (convs1, convs2, convs4) <- do + -- FUTUREWORKI(fisx): implement this (create both team convs and regular convs) + pure (Nothing, Nothing, Nothing) + + let usr2contacts = Just $ (,"accepted") <$> [ejpd4] + usr3contacts = Just $ (,"accepted") <$> [ejpd5] + usr4contacts = Just $ (,"accepted") <$> [ejpd2, ejpd5] + usr5contacts = Just $ (,"accepted") <$> [ejpd3, ejpd4] + + ejpd0 = mkUsr owner1 Nothing [] Nothing (Just ([ejpd1, ejpd2], "list_complete")) Nothing Nothing + ejpd1 = mkUsr usr1 (Just handle1) toks1 Nothing (Just ([ejpd0, ejpd2], "list_complete")) convs1 (Just assets1) + ejpd2 = mkUsr usr2 (Just handle2) toks2 usr2contacts (Just ([ejpd0, ejpd1], "list_complete")) convs2 (Just assets2) + ejpd3 = mkUsr usr3 (Just handle3) [] usr3contacts Nothing Nothing (Just assets3) + ejpd4 = mkUsr usr4 (Just handle4) toks4 usr4contacts Nothing convs4 (Just assets4) + ejpd5 = mkUsr usr5 (Just handle5) [] usr5contacts Nothing Nothing Nothing + + pure (ejpd1, ejpd2, ejpd3, ejpd4, ejpd5) + where + -- Return value is a 'EJPDResponseItem'. + mkUsr :: + HasCallStack => + A.Value {- user -} -> + Maybe String {- handle (in case usr is not up to date, we pass this separately) -} -> + [String {- push tokens -}] -> + Maybe [(A.Value {- ejpd response item of contact -}, String {- relation -})] -> + Maybe ([A.Value {- ejpd response item -}], String {- pagination flag -}) -> + Maybe [(String {- conv name -}, String {- conv id -})] -> + Maybe [String {- asset url -}] -> + A.Value + mkUsr usr handle toks contacts teamContacts convs assets = result + where + result = + object + [ -- (We know we have "id", but using ^? instead of ^. avoids the need for a Monoid instance for Value.) + "ejpd_response_user_id" .= (usr ^? key (fromString "id")), + "ejpd_response_team_id" .= (usr ^? key (fromString "team")), + "ejpd_response_name" .= (usr ^? key (fromString "name")), + "ejpd_response_handle" .= handle, + "ejpd_response_email" .= (usr ^? key (fromString "email")), + "ejpd_response_phone" .= (usr ^? key (fromString "phone")), + "ejpd_response_push_tokens" .= toks, + "ejpd_response_contacts" .= (trimContacts _1 <$> contacts), + "ejpd_response_team_contacts" .= (teamContacts & _Just . _1 %~ trimContacts id), + "ejpd_response_conversations" .= convs, + "ejpd_response_assets" .= assets + ] + + trimContacts :: forall x. Lens' x A.Value -> [x] -> [x] + trimContacts lns = + fmap + ( lns + %~ ( \case + trimmable@(A.Object _) -> trimItem trimmable + other -> error $ show other + ) + ) + + trimItem :: A.Value -> A.Value + trimItem = + (key (fromString "ejpd_response_contacts") .~ A.Null) + . (key (fromString "ejpd_response_team_contacts") .~ A.Null) + . (key (fromString "ejpd_response_conversations") .~ A.Null) + +testEJPDRequest :: HasCallStack => App () +testEJPDRequest = do + (usr1, usr2, usr3, usr4, usr5) <- setupEJPD + + let check :: HasCallStack => [A.Value] -> App () + check want = do + let handle = cs . (^?! (key (fromString "ejpd_response_handle") . _String)) + have <- BI.getEJPDInfo OwnDomain (handle <$> want) "include_contacts" + have.json `shouldMatchSpecial` object ["ejpd_response" .= want] + + shouldMatchSpecial :: (MakesValue a, MakesValue b, HasCallStack) => a -> b -> App () + shouldMatchSpecial = shouldMatchWithRules [minBound ..] resolveAssetLinks + + -- query params and even the uuid in the path of asset urls may differ between actual + -- and expected value because they are re-generated non-deterministically. so we fetch + -- the actual content. + resolveAssetLinks :: A.Value -> App (Maybe A.Value) + resolveAssetLinks = \case + (A.String (cs -> url)) | isProbablyAssetUrl url -> (Just . toJSON) <$> fetchIt url + _ -> pure Nothing + where + isProbablyAssetUrl :: String -> Bool + isProbablyAssetUrl url = all (`isInfixOf` url) ["http", "://", "/dummy-bucket/v3/persistent/"] + + fetchIt :: String -> App String + fetchIt url = liftIO $ (cs . view Wreq.responseBody) <$> Wreq.get url + + check [usr1] + check [usr2] + check [usr3] + check [usr4, usr5] diff --git a/integration/test/Testlib/Assertions.hs b/integration/test/Testlib/Assertions.hs index 2668a84b74..ac86c96214 100644 --- a/integration/test/Testlib/Assertions.hs +++ b/integration/test/Testlib/Assertions.hs @@ -2,11 +2,17 @@ module Testlib.Assertions where +import Control.Applicative ((<|>)) import Control.Exception as E +import Control.Lens ((^?)) +import qualified Control.Lens.Plated as LP import Control.Monad.Reader import Data.Aeson (Value) import qualified Data.Aeson as Aeson +import qualified Data.Aeson.Diff as AD import qualified Data.Aeson.Encode.Pretty as Aeson +import qualified Data.Aeson.KeyMap as Aeson +import Data.Aeson.Lens (_Array, _Object) import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Lazy as BS import Data.Char @@ -14,6 +20,7 @@ import Data.Foldable import Data.Hex import Data.List import qualified Data.Map as Map +import Data.Maybe (isJust, mapMaybe) import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import qualified Data.Text.Lazy as TL @@ -52,13 +59,94 @@ shouldMatch :: -- | The expected value b -> App () -a `shouldMatch` b = do +shouldMatch = shouldMatchWithMsg Nothing + +shouldMatchWithMsg :: + (MakesValue a, MakesValue b, HasCallStack) => + -- | Message to be added to failure report + Maybe String -> + -- | The actual value + a -> + -- | The expected value + b -> + App () +shouldMatchWithMsg msg a b = do xa <- make a xb <- make b unless (xa == xb) do pa <- prettyJSON xa pb <- prettyJSON xb - assertFailure $ "Actual:\n" <> pa <> "\nExpected:\n" <> pb + diff <- -- show diff, but only in the interesting cases. + if (isJust (xa ^? _Object) && isJust (xb ^? _Object)) + || (isJust (xa ^? _Array) && isJust (xb ^? _Array)) + then ("\nDiff:\n" <>) <$> prettyJSON (AD.diff xa xb) + else pure "" + assertFailure $ (maybe "" (<> "\n") msg) <> "Actual:\n" <> pa <> "\nExpected:\n" <> pb <> diff + +-- | apply some canonicalization transformations that *usually* do not change semantics before +-- comparing. +shouldMatchLeniently :: (MakesValue a, MakesValue b, HasCallStack) => a -> b -> App () +shouldMatchLeniently = shouldMatchWithRules [EmptyArrayIsNull, RemoveNullFieldsFromObjects] (const $ pure Nothing) + +-- | apply *all* canonicalization transformations before comparing. some of these may not be +-- valid on your input, see 'LenientMatchRule' for details. +shouldMatchSloppily :: (MakesValue a, MakesValue b, HasCallStack) => a -> b -> App () +shouldMatchSloppily = shouldMatchWithRules [minBound ..] (const $ pure Nothing) + +-- | apply *all* canonicalization transformations before comparing. some of these may not be +-- valid on your input, see 'LenientMatchRule' for details. +shouldMatchALittle :: (MakesValue a, MakesValue b, HasCallStack) => (Aeson.Value -> App (Maybe Aeson.Value)) -> a -> b -> App () +shouldMatchALittle = shouldMatchWithRules [minBound ..] + +data LenientMatchRule + = EmptyArrayIsNull + | ArraysAreSets + | RemoveNullFieldsFromObjects + deriving (Eq, Ord, Show, Bounded, Enum) + +shouldMatchWithRules :: + (MakesValue a, MakesValue b, HasCallStack) => + [LenientMatchRule] -> + (Aeson.Value -> App (Maybe Aeson.Value)) -> + a -> + b -> + App () +shouldMatchWithRules rules customRules a b = do + xa <- make a + xb <- make b + simplify xa `shouldMatch` simplify xb + where + simplify :: Aeson.Value -> App Aeson.Value + simplify = LP.rewriteM $ (\v -> foldM (tryApplyRule v) Nothing compiledRules) + + tryApplyRule :: + Aeson.Value -> + Maybe Aeson.Value -> + (Aeson.Value -> App (Maybe Aeson.Value)) -> + App (Maybe Aeson.Value) + tryApplyRule v bresult arule = (bresult <|>) <$> arule v + + compiledRules :: [Aeson.Value -> App (Maybe Aeson.Value)] + compiledRules = customRules : ((\r v -> pure $ runRule r v) <$> rules) + + runRule :: LenientMatchRule -> Aeson.Value -> Maybe Aeson.Value + runRule EmptyArrayIsNull = \case + Aeson.Array arr + | arr == mempty -> + Just Aeson.Null + _ -> Nothing + runRule ArraysAreSets = \case + Aeson.Array (toList -> arr) -> + let arr' = sort arr + in if arr == arr' then Nothing else Just $ Aeson.toJSON arr' + _ -> Nothing + runRule RemoveNullFieldsFromObjects = \case + Aeson.Object (Aeson.toList -> obj) + | any ((== Aeson.Null) . snd) obj -> + let rmNulls (_, Aeson.Null) = Nothing + rmNulls (k, v) = Just (k, v) + in Just . Aeson.Object . Aeson.fromList $ mapMaybe rmNulls obj + _ -> Nothing shouldMatchBase64 :: (MakesValue a, MakesValue b, HasCallStack) => 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 93db38b297..d34bd9fb78 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 @@ -21,13 +21,26 @@ module Wire.API.Routes.Internal.Brig.EJPD ( EJPDRequestBody (EJPDRequestBody, ejpdRequestBody), EJPDResponseBody (EJPDResponseBody, ejpdResponseBody), - EJPDResponseItem (EJPDResponseItem, ejpdResponseHandle, ejpdResponsePushTokens, ejpdResponseContacts), + EJPDResponseItem + ( EJPDResponseItem, + ejpdResponseUserId, + ejpdResponseTeamId, + ejpdResponseName, + ejpdResponseHandle, + ejpdResponseEmail, + ejpdResponsePhone, + ejpdResponsePushTokens, + ejpdResponseContacts, + ejpdResponseTeamContacts, + ejpdResponseConversations, + ejpdResponseAssets + ), ) where import Data.Aeson hiding (json) import Data.Handle (Handle) -import Data.Id (TeamId, UserId) +import Data.Id (ConvId, TeamId, UserId) import Data.OpenApi (ToSchema) import Deriving.Swagger (CamelToSnake, CustomSwagger (..), FieldLabelModifier, StripSuffix) import Imports hiding (head) @@ -57,7 +70,9 @@ data EJPDResponseItem = EJPDResponseItem ejpdResponsePhone :: Maybe Phone, ejpdResponsePushTokens :: Set Text, -- 'Wire.API.Push.V2.Token.Token', but that would produce an orphan instance. ejpdResponseContacts :: Maybe (Set (Relation, EJPDResponseItem)), - ejpdResponseTeamContacts :: Maybe (Set EJPDResponseItem, NewListType) + ejpdResponseTeamContacts :: Maybe (Set EJPDResponseItem, NewListType), + ejpdResponseConversations :: Maybe (Set (Text, ConvId)), -- name, id + ejpdResponseAssets :: Maybe (Set Text) -- urls pointing to s3 resources } deriving stock (Eq, Ord, Show, Generic) deriving (Arbitrary) via (GenericUniform EJPDResponseItem) @@ -86,7 +101,9 @@ instance ToJSON EJPDResponseItem where "ejpd_response_phone" .= ejpdResponsePhone rspi, "ejpd_response_push_tokens" .= ejpdResponsePushTokens rspi, "ejpd_response_contacts" .= ejpdResponseContacts rspi, - "ejpd_response_team_contacts" .= ejpdResponseTeamContacts rspi + "ejpd_response_team_contacts" .= ejpdResponseTeamContacts rspi, + "ejpd_response_conversations" .= ejpdResponseConversations rspi, + "ejpd_response_assets" .= ejpdResponseAssets rspi ] instance FromJSON EJPDResponseItem where @@ -101,3 +118,5 @@ instance FromJSON EJPDResponseItem where <*> obj .: "ejpd_response_push_tokens" <*> obj .:? "ejpd_response_contacts" <*> obj .:? "ejpd_response_team_contacts" + <*> obj .:? "ejpd_response_conversations" + <*> obj .:? "ejpd_response_assets" 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 cb9599b441..592e72dc61 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs @@ -22,12 +22,15 @@ import Data.OpenApi import Imports import Servant import Servant.OpenApi +import Wire.API.Asset import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named type InternalAPI = "i" - :> "status" - :> MultiVerb 'GET '() '[RespondEmpty 200 "OK"] () + :> ( "status" :> MultiVerb 'GET '() '[RespondEmpty 200 "OK"] () + :<|> Named "iGetAsset" ("assets" :> Capture "key" AssetKey :> Get '[Servant.JSON] Text) + ) swaggerDoc :: OpenApi swaggerDoc = diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 659db42be1..c04173cf91 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -100,6 +100,7 @@ import Wire.API.User.Activation import Wire.API.User.Client import Wire.API.User.RichInfo import Wire.NotificationSubsystem +import Wire.Rpc import Wire.Sem.Concurrency import Wire.Sem.Paging.Cassandra (InternalPaging) @@ -108,20 +109,21 @@ import Wire.Sem.Paging.Cassandra (InternalPaging) servantSitemap :: forall r p. - ( Member BlacklistStore r, + ( Member BlacklistPhonePrefixStore r, + Member BlacklistStore r, Member CodeStore r, - Member BlacklistPhonePrefixStore r, - Member PasswordResetStore r, - Member GalleyProvider r, - Member (UserPendingActivationStore p) r, - Member FederationConfigStore r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, Member (Concurrency 'Unsafe) r, + Member (ConnectionStore InternalPaging) r, + Member (Embed HttpClientIO) r, + Member FederationConfigStore r, + Member GalleyProvider r, Member (Input (Local ())) r, Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member NotificationSubsystem r, + Member PasswordResetStore r, + Member Rpc r, + Member TinyLog r, + Member (UserPendingActivationStore p) r ) => ServerT BrigIRoutes.API (Handler r) servantSitemap = @@ -142,7 +144,10 @@ istatusAPI :: forall r. ServerT BrigIRoutes.IStatusAPI (Handler r) istatusAPI = Named @"get-status" (pure NoContent) ejpdAPI :: - (Member GalleyProvider r, Member NotificationSubsystem r) => + ( Member GalleyProvider r, + Member NotificationSubsystem r, + Member Rpc r + ) => ServerT BrigIRoutes.EJPD_API (Handler r) ejpdAPI = Brig.User.EJPD.ejpdRequest diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 1b41a47380..4467ad7c34 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -37,6 +37,7 @@ module Brig.App galley, galleyEndpoint, gundeckEndpoint, + cargoholdEndpoint, federator, casClient, userTemplates, @@ -164,6 +165,7 @@ data Env = Env _galley :: RPC.Request, _galleyEndpoint :: Endpoint, _gundeckEndpoint :: Endpoint, + _cargoholdEndpoint :: Endpoint, _federator :: Maybe Endpoint, -- FUTUREWORK: should we use a better type here? E.g. to avoid fresh connections all the time? _casClient :: Cas.ClientState, _smtpEnv :: Maybe SMTP.SMTP, @@ -264,6 +266,7 @@ newEnv o = do _galley = mkEndpoint $ Opt.galley o, _galleyEndpoint = Opt.galley o, _gundeckEndpoint = Opt.gundeck o, + _cargoholdEndpoint = Opt.cargohold o, _federator = Opt.federatorInternal o, _casClient = cas, _smtpEnv = emailSMTP, diff --git a/services/brig/src/Brig/User/EJPD.hs b/services/brig/src/Brig/User/EJPD.hs index ae7538b6b5..31392bfd84 100644 --- a/services/brig/src/Brig/User/EJPD.hs +++ b/services/brig/src/Brig/User/EJPD.hs @@ -20,6 +20,8 @@ -- manually.) module Brig.User.EJPD (ejpdRequest) where +import Bilge.Request +import Bilge.Response import Brig.API.Handler import Brig.API.User (lookupHandle) import Brig.App @@ -27,50 +29,54 @@ import Brig.Data.Connection qualified as Conn import Brig.Data.User (lookupUser) import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.Effects.GalleyProvider qualified as GalleyProvider -import Brig.Types.User (HavePendingInvitations (NoPendingInvitations)) import Control.Error hiding (bool) import Control.Lens (view, (^.)) +import Data.Aeson qualified as A +import Data.ByteString.Conversion import Data.Handle (Handle) import Data.Id (UserId) import Data.Set qualified as Set import Imports hiding (head) -import Polysemy +import Network.HTTP.Types.Method +import Polysemy (Member) 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)) import Wire.API.Team.Member qualified as Team -import Wire.API.User (User, userDisplayName, userEmail, userHandle, userId, userPhone, userTeam) +import Wire.API.User import Wire.NotificationSubsystem +import Wire.Rpc ejpdRequest :: forall r. ( Member GalleyProvider r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member Rpc r ) => Maybe Bool -> EJPDRequestBody -> - Handler r EJPDResponseBody -ejpdRequest includeContacts (EJPDRequestBody handles) = do - ExceptT $ Right . EJPDResponseBody . catMaybes <$> forM handles (go1 (fromMaybe False includeContacts)) + (Handler r) EJPDResponseBody +ejpdRequest (fromMaybe False -> includeContacts) (EJPDRequestBody handles) = do + ExceptT $ Right . EJPDResponseBody . catMaybes <$> forM handles go1 where -- find uid given handle - go1 :: Bool -> Handle -> (AppT r) (Maybe EJPDResponseItem) - go1 includeContacts' handle = do + go1 :: Handle -> (AppT r) (Maybe EJPDResponseItem) + go1 handle = do mbUid <- wrapClient $ lookupHandle handle mbUsr <- maybe (pure Nothing) (wrapClient . lookupUser NoPendingInvitations) mbUid - maybe (pure Nothing) (fmap Just . go2 includeContacts') mbUsr + maybe (pure Nothing) (fmap Just . go2 includeContacts) mbUsr -- construct response item given uid go2 :: Bool -> User -> (AppT r) EJPDResponseItem - go2 includeContacts' target = do + go2 reallyIncludeContacts target = do let uid = userId target ptoks <- PushTok.tokenText . view PushTok.token <$$> liftSem (getPushTokens uid) mbContacts <- - if includeContacts' + if reallyIncludeContacts then do contacts :: [(UserId, RelationWithHistory)] <- wrapClient $ Conn.lookupContactListWithRelation uid @@ -85,7 +91,7 @@ ejpdRequest includeContacts (EJPDRequestBody handles) = do pure Nothing mbTeamContacts <- - case (includeContacts', userTeam target) of + case (reallyIncludeContacts, userTeam target) of (True, Just tid) -> do memberList <- liftSem $ GalleyProvider.getTeamMembers tid let members = (view Team.userId <$> (memberList ^. Team.teamMembers)) \\ [uid] @@ -99,6 +105,28 @@ ejpdRequest includeContacts (EJPDRequestBody handles) = do _ -> do pure Nothing + mbConversations <- do + -- FUTUREWORK(fisx) + pure Nothing + + mbAssets <- do + urls <- forM (userAssets target) $ \(asset :: Asset) -> do + cgh <- asks (view cargoholdEndpoint) + let key = toByteString' $ assetKey asset + resp <- liftSem $ rpcWithRetries "cargohold" cgh (method GET . paths ["/i/assets", key]) + pure $ + case (statusCode resp, responseJsonEither resp) of + (200, Right (A.String loc)) -> loc + _ -> + cs $ + "could not fetch asset: " + <> show key + <> ", error: " + <> show (statusCode resp, responseBody resp) + pure $ case urls of + [] -> Nothing + something -> Just (Set.fromList something) + pure $ EJPDResponseItem uid @@ -110,3 +138,5 @@ ejpdRequest includeContacts (EJPDRequestBody handles) = do (Set.fromList ptoks) mbContacts mbTeamContacts + mbConversations + mbAssets diff --git a/services/brig/test/integration/API/Internal.hs b/services/brig/test/integration/API/Internal.hs index b0f7704123..b4d730fcd9 100644 --- a/services/brig/test/integration/API/Internal.hs +++ b/services/brig/test/integration/API/Internal.hs @@ -34,13 +34,13 @@ import Cassandra qualified as Cass import Cassandra.Util import Control.Exception (ErrorCall (ErrorCall), throwIO) import Control.Lens ((^.), (^?!)) +import Data.Aeson qualified as Aeson import Data.Aeson.Lens qualified as Aeson import Data.Aeson.Types qualified as Aeson import Data.ByteString.Conversion (toByteString') import Data.Default import Data.Id import Data.Qualified -import Data.Set qualified as Set import GHC.TypeLits (KnownSymbol) import Imports import System.IO.Temp @@ -48,20 +48,16 @@ import Test.Tasty import Test.Tasty.HUnit import Util import Util.Options (Endpoint) -import Wire.API.Connection qualified as Conn -import Wire.API.Routes.Internal.Brig import Wire.API.Team.Feature import Wire.API.Team.Feature qualified as ApiFt -import Wire.API.Team.Member qualified as Team import Wire.API.User import Wire.API.User.Client tests :: Opt.Opts -> Manager -> Cass.ClientState -> Brig -> Endpoint -> Gundeck -> Galley -> IO TestTree -tests opts mgr db brig brigep gundeck galley = do +tests opts mgr db brig brigep _gundeck galley = do pure $ testGroup "api/internal" $ - [ test mgr "ejpd requests" $ testEJPDRequest mgr brig brigep gundeck, - test mgr "account features: conferenceCalling" $ + [ test mgr "account features: conferenceCalling" $ testFeatureConferenceCallingByAccount opts mgr db brig brigep galley, test mgr "suspend and unsuspend user" $ testSuspendUser db brig, test mgr "suspend non existing user and verify no db entry" $ @@ -98,54 +94,6 @@ setAccountStatus brig u s = . json (AccountStatusUpdate s) ) -testEJPDRequest :: (TestConstraints m) => Manager -> Brig -> Endpoint -> Gundeck -> m () -testEJPDRequest mgr brig brigep gundeck = do - (handle1, mkUsr1, handle2, mkUsr2, mkUsr3) <- scaffolding brig gundeck - - do - let req = EJPDRequestBody [handle1] - want = - EJPDResponseBody - [ mkUsr1 Nothing Nothing - ] - have <- ejpdRequestClient brigep mgr Nothing req - liftIO $ assertEqual "" want have - - do - let req = EJPDRequestBody [handle1, handle2] - want = - EJPDResponseBody - [ mkUsr1 Nothing Nothing, - mkUsr2 Nothing Nothing - ] - have <- ejpdRequestClient brigep mgr Nothing req - liftIO $ assertEqual "" want have - - do - let req = EJPDRequestBody [handle2] - want = - EJPDResponseBody - [ mkUsr2 - (Just (Set.fromList [(Conn.Accepted, mkUsr1 Nothing Nothing)])) - Nothing - ] - have <- ejpdRequestClient brigep mgr (Just True) req - liftIO $ assertEqual "" want have - - do - let req = EJPDRequestBody [handle1, handle2] - want = - EJPDResponseBody - [ mkUsr1 - (Just (Set.fromList [(Conn.Accepted, mkUsr2 Nothing Nothing)])) - (Just (Set.fromList [mkUsr3 Nothing Nothing], Team.NewListComplete)), - mkUsr2 - (Just (Set.fromList [(Conn.Accepted, mkUsr1 Nothing Nothing)])) - Nothing - ] - have <- ejpdRequestClient brigep mgr (Just True) req - liftIO $ assertEqual "" want have - testFeatureConferenceCallingByAccount :: forall m. (TestConstraints m) => Opt.Opts -> Manager -> Cass.ClientState -> Brig -> Endpoint -> Galley -> m () testFeatureConferenceCallingByAccount (Opt.optSettings -> settings) mgr db brig brigep galley = do let check :: (HasCallStack) => ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig -> m () diff --git a/services/brig/test/integration/API/Internal/Util.hs b/services/brig/test/integration/API/Internal/Util.hs index 5a55b04461..733c23620b 100644 --- a/services/brig/test/integration/API/Internal/Util.hs +++ b/services/brig/test/integration/API/Internal/Util.hs @@ -20,108 +20,27 @@ module API.Internal.Util ( TestConstraints, - MkUsr, - scaffolding, - ejpdRequestClient, getAccountConferenceCallingConfigClient, putAccountConferenceCallingConfigClient, deleteAccountConferenceCallingConfigClient, ) where -import API.Team.Util (createPopulatedBindingTeamWithNamesAndHandles) import Bilge hiding (host, port) -import Control.Lens (view, (^.)) -import Control.Monad.Catch (MonadCatch, MonadThrow, throwM) -import Data.ByteString.Base16 qualified as B16 -import Data.Handle (Handle) +import Control.Lens ((^.)) +import Control.Monad.Catch (MonadCatch) import Data.Id -import Data.List1 qualified as List1 import Data.Proxy (Proxy (Proxy)) -import Data.Set qualified as Set -import Data.Text.Encoding qualified as T import Imports import Servant.API ((:>)) import Servant.API.ContentTypes (NoContent) import Servant.Client qualified as Client -import System.Random (randomIO) -import Util import Util.Options (Endpoint, host, port) -import Wire.API.Connection -import Wire.API.Push.V2.Token qualified as PushToken import Wire.API.Routes.Internal.Brig as IAPI import Wire.API.Team.Feature qualified as Public -import Wire.API.Team.Member qualified as Team -import Wire.API.User type TestConstraints m = (MonadFail m, MonadCatch m, MonadIO m, MonadHttp m) -type MkUsr = - Maybe (Set (Relation, EJPDResponseItem)) -> - Maybe (Set EJPDResponseItem, Team.NewListType) -> - EJPDResponseItem - -scaffolding :: - forall m. - (TestConstraints m) => - Brig -> - Gundeck -> - m (Handle, MkUsr, Handle, MkUsr, MkUsr) -scaffolding brig gundeck = do - (_tid, usr1, [usr3]) <- createPopulatedBindingTeamWithNamesAndHandles brig 1 - (_handle1, usr2) <- createUserWithHandle brig - connectUsers brig (userId usr1) (List1.singleton $ userId usr2) - tok1 <- registerPushToken gundeck $ userId usr1 - tok2 <- registerPushToken gundeck $ userId usr2 - tok3 <- registerPushToken gundeck $ userId usr2 - pure - ( fromJust $ userHandle usr1, - mkUsr usr1 (Set.fromList [tok1]), - fromJust $ userHandle usr2, - mkUsr usr2 (Set.fromList [tok2, tok3]), - mkUsr usr3 Set.empty - ) - where - mkUsr :: User -> Set Text -> MkUsr - mkUsr usr toks = - EJPDResponseItem - (userId usr) - (userTeam usr) - (userDisplayName usr) - (userHandle usr) - (userEmail usr) - (userPhone usr) - toks - - registerPushToken :: Gundeck -> UserId -> m Text - registerPushToken gd u = do - t <- randomToken - rsp <- registerPushTokenRequest gd u t - responseJsonEither rsp - & either - (error . show) - (pure . PushToken.tokenText . view PushToken.token) - - registerPushTokenRequest :: Gundeck -> UserId -> PushToken.PushToken -> m ResponseLBS - registerPushTokenRequest gd u t = do - post - ( gd - . path "/push/tokens" - . contentJson - . zUser u - . zConn "random" - . json t - ) - - randomToken :: m PushToken.PushToken - randomToken = liftIO $ do - c <- liftIO $ ClientId <$> (randomIO :: IO Word64) - tok <- (PushToken.Token . T.decodeUtf8) . B16.encode <$> randomBytes 32 - pure $ PushToken.pushToken PushToken.APNSSandbox (PushToken.AppName "test") tok c - -ejpdRequestClientM :: Maybe Bool -> EJPDRequestBody -> Client.ClientM EJPDResponseBody -ejpdRequestClientM = Client.client (Proxy @("i" :> IAPI.EJPDRequest)) - getAccountConferenceCallingConfigClientM :: UserId -> Client.ClientM (Public.WithStatusNoLock Public.ConferenceCallingConfig) getAccountConferenceCallingConfigClientM = Client.client (Proxy @("i" :> IAPI.GetAccountConferenceCallingConfig)) @@ -131,9 +50,6 @@ putAccountConferenceCallingConfigClientM = Client.client (Proxy @("i" :> IAPI.Pu deleteAccountConferenceCallingConfigClientM :: UserId -> Client.ClientM NoContent deleteAccountConferenceCallingConfigClientM = Client.client (Proxy @("i" :> IAPI.DeleteAccountConferenceCallingConfig)) -ejpdRequestClient :: (HasCallStack, MonadThrow m, MonadIO m) => Endpoint -> Manager -> Maybe Bool -> EJPDRequestBody -> m EJPDResponseBody -ejpdRequestClient brigep mgr includeContacts ejpdReqBody = runHereClientM brigep mgr (ejpdRequestClientM includeContacts ejpdReqBody) >>= either throwM pure - getAccountConferenceCallingConfigClient :: (HasCallStack, MonadIO m) => Endpoint -> Manager -> UserId -> m (Either Client.ClientError (Public.WithStatusNoLock Public.ConferenceCallingConfig)) getAccountConferenceCallingConfigClient brigep mgr uid = runHereClientM brigep mgr (getAccountConferenceCallingConfigClientM uid) diff --git a/services/cargohold/src/CargoHold/API/Public.hs b/services/cargohold/src/CargoHold/API/Public.hs index 4435fc9e3e..0430c110ef 100644 --- a/services/cargohold/src/CargoHold/API/Public.hs +++ b/services/cargohold/src/CargoHold/API/Public.hs @@ -27,6 +27,7 @@ import qualified CargoHold.Types.V3 as V3 import Control.Lens import Control.Monad.Trans.Except (throwE) import Data.ByteString.Builder +import qualified Data.ByteString.Builder as Builder import qualified Data.ByteString.Lazy as LBS import Data.Domain import Data.Id @@ -36,12 +37,13 @@ import Imports hiding (head) import qualified Network.HTTP.Types as HTTP import Servant.API import Servant.Server hiding (Handler) -import URI.ByteString +import URI.ByteString as URI import Wire.API.Asset import Wire.API.Federation.API import Wire.API.Routes.AssetBody import Wire.API.Routes.Internal.Brig (brigInternalClient) import Wire.API.Routes.Internal.Cargohold +import Wire.API.Routes.Named import Wire.API.Routes.Public.Cargohold import Wire.API.User (AccountStatus (Active), AccountStatusResp (..)) @@ -74,7 +76,19 @@ servantSitemap = :<|> deleteAssetV4 internalSitemap :: ServerT InternalAPI Handler -internalSitemap = pure () +internalSitemap = + pure () + :<|> Named @"iGetAsset" iDownloadAssetV3 + +-- | Like 'downloadAssetV3' below, but it works without user session token, and has a +-- different route type. +iDownloadAssetV3 :: V3.AssetKey -> Handler Text +iDownloadAssetV3 key = do + render <$> V3.downloadUnsafe key Nothing + where + -- (NB: don't use HttpsUrl here, as in some test environments we legitimately use "http"!) + render :: URI.URI -> Text + render = cs . Builder.toLazyByteString . URI.serializeURIRef class HasLocation (tag :: PrincipalTag) where assetLocation :: Local AssetKey -> [Text] diff --git a/services/cargohold/src/CargoHold/API/V3.hs b/services/cargohold/src/CargoHold/API/V3.hs index d96d772d5c..4b4c58f374 100644 --- a/services/cargohold/src/CargoHold/API/V3.hs +++ b/services/cargohold/src/CargoHold/API/V3.hs @@ -18,6 +18,7 @@ module CargoHold.API.V3 ( upload, download, + downloadUnsafe, checkMetadata, delete, renewToken, @@ -112,6 +113,9 @@ download own key tok mbHost = runMaybeT $ do checkMetadata (Just own) key tok lift $ genSignedURL (S3.mkKey key) mbHost +downloadUnsafe :: V3.AssetKey -> Maybe Text -> Handler URI +downloadUnsafe key mbHost = genSignedURL (S3.mkKey key) mbHost + checkMetadata :: Maybe V3.Principal -> V3.AssetKey -> Maybe V3.AssetToken -> MaybeT Handler () checkMetadata mown key tok = do s3 <- lift (S3.getMetadataV3 key) >>= maybe mzero pure