diff --git a/changelog.d/1-api-changes/qualified-assets b/changelog.d/1-api-changes/qualified-assets new file mode 100644 index 0000000000..62e988dde3 --- /dev/null +++ b/changelog.d/1-api-changes/qualified-assets @@ -0,0 +1 @@ +Add qualified v4 endpoints for downloading and deleting assets. The upload API is still on the same path, but the asset object it returns now contains a `domain` field. Note that federated behaviour is still not implemented. diff --git a/libs/cargohold-types/src/CargoHold/Types/V3.hs b/libs/cargohold-types/src/CargoHold/Types/V3.hs index b4c3f2531e..e0b8ee3fe1 100644 --- a/libs/cargohold-types/src/CargoHold/Types/V3.hs +++ b/libs/cargohold-types/src/CargoHold/Types/V3.hs @@ -58,7 +58,7 @@ where import Data.ByteString.Conversion import Data.Id import Imports -import Wire.API.Asset.V3 +import Wire.API.Asset -------------------------------------------------------------------------------- -- Principal diff --git a/libs/wire-api/src/Wire/API/Asset.hs b/libs/wire-api/src/Wire/API/Asset.hs index 94522eba44..49044a4e15 100644 --- a/libs/wire-api/src/Wire/API/Asset.hs +++ b/libs/wire-api/src/Wire/API/Asset.hs @@ -1,3 +1,7 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE StrictData #-} +{-# LANGUAGE TemplateHaskell #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2020 Wire Swiss GmbH @@ -16,8 +20,405 @@ -- with this program. If not, see . module Wire.API.Asset - ( module V3, + ( -- * Asset + Asset, + Asset', + mkAsset, + assetKey, + assetExpires, + assetToken, + + -- * AssetKey + AssetKey (..), + assetKeyToText, + + -- * AssetToken + AssetToken (..), + NewAssetToken (..), + + -- * Body Construction + buildMultipartBody, + beginMultipartBody, + endMultipartBody, + + -- * AssetHeaders + AssetHeaders (..), + mkHeaders, + + -- * AssetSettings + AssetSettings, + defAssetSettings, + setAssetPublic, + setAssetRetention, + AssetRetention (..), + assetRetentionSeconds, + assetExpiringSeconds, + assetVolatileSeconds, + retentionToTextRep, + + -- * Streaming + AssetLocation (..), + LocalOrRemoteAsset (..), ) where -import Wire.API.Asset.V3 as V3 +import qualified Codec.MIME.Type as MIME +import Control.Lens (makeLenses, (?~)) +import Data.Aeson (FromJSON (..), ToJSON (..)) +import qualified Data.Aeson as Aeson +import Data.Attoparsec.ByteString.Char8 hiding (I) +import Data.Bifunctor +import Data.ByteString.Builder +import Data.ByteString.Conversion +import qualified Data.ByteString.Lazy as LBS +import Data.Id +import Data.Json.Util (UTCTimeMillis (fromUTCTimeMillis), toUTCTimeMillis) +import Data.Proxy +import Data.Qualified +import Data.SOP +import Data.Schema +import qualified Data.Swagger as S +import qualified Data.Text as T +import Data.Text.Ascii (AsciiBase64Url) +import qualified Data.Text.Encoding as T +import qualified Data.Text.Encoding.Error as T +import Data.Time.Clock +import qualified Data.UUID as UUID +import GHC.TypeLits +import Imports +import Servant +import URI.ByteString +import Wire.API.Arbitrary (Arbitrary (..), GenericUniform (..)) +import Wire.API.ErrorDescription +import Wire.API.Routes.MultiVerb + +-------------------------------------------------------------------------------- +-- Asset + +type Asset = Asset' (Qualified AssetKey) + +-- | A newly uploaded asset. +data Asset' key = Asset + { _assetKey :: key, + _assetExpires :: Maybe UTCTime, + _assetToken :: Maybe AssetToken + } + deriving stock (Eq, Show, Generic, Functor) + +deriving via Schema (Asset' key) instance ToSchema (Asset' key) => (ToJSON (Asset' key)) + +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)) + +-- Generate expiry time with millisecond precision +instance Arbitrary key => Arbitrary (Asset' key) where + arbitrary = Asset <$> arbitrary <*> (fmap milli <$> arbitrary) <*> arbitrary + where + milli = fromUTCTimeMillis . toUTCTimeMillis + +mkAsset :: key -> Asset' key +mkAsset k = Asset k Nothing Nothing + +instance ToSchema Asset where + schema = + object "Asset" $ + Asset + <$> _assetKey + .= ( Qualified + <$> qUnqualified .= field "key" schema + <*> qDomain .= field "domain" schema + ) + <*> (fmap toUTCTimeMillis . _assetExpires) + .= maybe_ + (optField "expires" (fromUTCTimeMillis <$> schema)) + <*> _assetToken .= maybe_ (optField "token" schema) + +-------------------------------------------------------------------------------- +-- AssetKey + +-- | A unique, versioned asset identifier. +-- Note: Can be turned into a sum type with additional constructors +-- for future versions. +data AssetKey = AssetKeyV3 AssetId AssetRetention + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform AssetKey) + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema AssetKey) + +instance FromByteString AssetKey where + parser = do + v <- decimal + _ <- char '-' + case (v :: Word) of + 3 -> parseV3 + _ -> fail $ "Invalid asset version: " ++ show v + where + -- AssetKeyV3 ::= Retention "-" uuid + -- Retention ::= decimal + parseV3 = do + r <- parser + _ <- char '-' + b <- takeByteString + case UUID.fromASCIIBytes b of + Just i -> return $! AssetKeyV3 (Id i) r + Nothing -> fail "Invalid asset ID" + +instance ToByteString AssetKey where + builder (AssetKeyV3 i r) = + builder '3' + <> builder '-' + <> builder r + <> builder '-' + <> builder (UUID.toASCIIBytes (toUUID i)) + +assetKeyToText :: AssetKey -> Text +assetKeyToText = T.decodeUtf8 . toByteString' + +instance ToSchema AssetKey where + schema = + (T.decodeUtf8 . toByteString') + .= parsedText "AssetKey" (runParser parser . T.encodeUtf8) + & doc' . S.schema . S.example ?~ toJSON ("3-1-47de4580-ae51-4650-acbb-d10c028cb0ac" :: Text) + +instance S.ToParamSchema AssetKey where + toParamSchema _ = S.toParamSchema (Proxy @Text) + +instance FromHttpApiData AssetKey where + parseUrlPiece = first T.pack . runParser parser . T.encodeUtf8 + +-------------------------------------------------------------------------------- +-- AssetToken + +-- | Asset tokens are bearer tokens that grant access to a single asset. +newtype AssetToken = AssetToken {assetTokenAscii :: AsciiBase64Url} + deriving stock (Eq, Show) + deriving newtype (FromByteString, ToByteString, Arbitrary) + deriving (FromJSON, ToJSON) via (Schema AssetToken) + +instance ToSchema AssetToken where + schema = + AssetToken <$> assetTokenAscii + .= schema + & doc' . S.schema . S.example ?~ toJSON ("aGVsbG8" :: Text) + +instance S.ToParamSchema AssetToken where + toParamSchema _ = S.toParamSchema (Proxy @Text) + +instance FromHttpApiData AssetToken where + parseUrlPiece = first T.pack . runParser parser . T.encodeUtf8 + +-- | A newly (re)generated token for an existing asset. +newtype NewAssetToken = NewAssetToken + {newAssetToken :: AssetToken} + deriving stock (Eq, Show) + deriving newtype (Arbitrary) + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema NewAssetToken) + +instance ToSchema NewAssetToken where + schema = + object "NewAssetToken" $ + NewAssetToken <$> newAssetToken .= field "token" schema + +-------------------------------------------------------------------------------- +-- Body Construction + +-- | Build a complete @multipart/mixed@ request body for a one-shot, +-- non-resumable asset upload. +buildMultipartBody :: AssetSettings -> MIME.Type -> LByteString -> Builder +buildMultipartBody sets typ bs = + let hdrs = mkHeaders typ bs + in beginMultipartBody sets hdrs <> lazyByteString bs <> endMultipartBody + +-- | Begin building a @multipart/mixed@ request body for a non-resumable upload. +-- The returned 'Builder' can be immediately followed by the actual asset bytes. +beginMultipartBody :: AssetSettings -> AssetHeaders -> Builder +beginMultipartBody sets (AssetHeaders t l) = + byteString + "--frontier\r\n\ + \Content-Type: application/json\r\n\ + \Content-Length: " + <> int64Dec (LBS.length settingsJson) + <> byteString + "\r\n\ + \\r\n" + <> lazyByteString settingsJson + <> byteString + "\r\n\ + \--frontier\r\n\ + \Content-Type: " + <> byteString (T.encodeUtf8 (MIME.showType t)) + <> byteString + "\r\n\ + \Content-Length: " + <> wordDec l + <> "\r\n\ + \\r\n" + where + settingsJson = Aeson.encode (schemaToJSON sets) + +-- | The trailer of a non-resumable @multipart/mixed@ request body initiated +-- via 'beginMultipartBody'. +endMultipartBody :: Builder +endMultipartBody = byteString "\r\n--frontier--\r\n" + +-------------------------------------------------------------------------------- +-- AssetHeaders + +-- | Headers provided during upload. +data AssetHeaders = AssetHeaders + { hdrType :: MIME.Type, + hdrLength :: Word + } + +mkHeaders :: MIME.Type -> LByteString -> AssetHeaders +mkHeaders t b = AssetHeaders t (fromIntegral (LBS.length b)) + +-------------------------------------------------------------------------------- +-- AssetSettings + +-- | Settings provided during upload. +data AssetSettings = AssetSettings + { _setAssetPublic :: Bool, + _setAssetRetention :: Maybe AssetRetention + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform AssetSettings) + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema AssetSettings) + +defAssetSettings :: AssetSettings +defAssetSettings = AssetSettings False Nothing + +instance ToSchema AssetSettings where + schema = + object "AssetSettings" $ + AssetSettings + <$> _setAssetPublic .= (fromMaybe False <$> optField "public" schema) + <*> _setAssetRetention .= maybe_ (optField "retention" schema) + +-------------------------------------------------------------------------------- +-- AssetRetention + +-- | The desired asset retention. +data AssetRetention + = -- | The asset is retained indefinitely. Typically used + -- for profile pictures / assets frequently accessed. + AssetEternal + | -- | DEPRECATED: should not be used by clients for new assets + -- The asset is retained indefinitely. + AssetPersistent + | -- | The asset is retained for a short period of time. + AssetVolatile + | -- | The asset is retained indefinitely, storage is optimised + -- for infrequent access + AssetEternalInfrequentAccess + | -- | The asset is retained for an extended period of time, + -- but not indefinitely. + AssetExpiring + deriving stock (Eq, Show, Enum, Bounded, Generic) + deriving (Arbitrary) via (GenericUniform AssetRetention) + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema AssetRetention) + +-- | The minimum TTL in seconds corresponding to a chosen retention. +assetRetentionSeconds :: AssetRetention -> Maybe NominalDiffTime +assetRetentionSeconds AssetEternal = Nothing +assetRetentionSeconds AssetPersistent = Nothing +assetRetentionSeconds AssetVolatile = Just assetVolatileSeconds +assetRetentionSeconds AssetEternalInfrequentAccess = Nothing +assetRetentionSeconds AssetExpiring = Just assetExpiringSeconds + +assetVolatileSeconds :: NominalDiffTime +assetVolatileSeconds = 28 * 24 * 3600 -- 28 days + +assetExpiringSeconds :: NominalDiffTime +assetExpiringSeconds = 365 * 24 * 3600 -- 365 days + +instance ToByteString AssetRetention where + builder AssetEternal = builder '1' + builder AssetPersistent = builder '2' + builder AssetVolatile = builder '3' + builder AssetEternalInfrequentAccess = builder '4' + builder AssetExpiring = builder '5' + +-- | ByteString representation is used in AssetKey +instance FromByteString AssetRetention where + parser = + decimal >>= \d -> case (d :: Word) of + 1 -> return AssetEternal + 2 -> return AssetPersistent + 3 -> return AssetVolatile + 4 -> return AssetEternalInfrequentAccess + 5 -> return AssetExpiring + _ -> fail $ "Invalid asset retention: " ++ show d + +retentionToTextRep :: AssetRetention -> Text +retentionToTextRep AssetEternal = "eternal" +retentionToTextRep AssetPersistent = "persistent" +retentionToTextRep AssetVolatile = "volatile" +retentionToTextRep AssetEternalInfrequentAccess = "eternal-infrequent_access" +retentionToTextRep AssetExpiring = "expiring" + +instance ToSchema AssetRetention where + schema = + enum @Text "AssetRetention" $ + foldMap + (\value -> element (retentionToTextRep value) value) + [minBound .. maxBound] + +-- FUTUREWORK: switch to a better URI library (e.g. modern-uri) +-- +-- This URI type is error-prone, since its internal representation is based on +-- ByteString, whereas URLs are defined in terms of characters, not octets (RFC +-- 3986). +newtype AssetLocation r = AssetLocation {getAssetLocation :: URIRef r} + +instance ToHttpApiData (AssetLocation r) where + toUrlPiece = T.decodeUtf8With T.lenientDecode . toHeader + toHeader = serializeURIRef' . getAssetLocation + +instance FromHttpApiData (AssetLocation Relative) where + parseUrlPiece = parseHeader . T.encodeUtf8 + parseHeader = + bimap (T.pack . show) AssetLocation + . parseRelativeRef strictURIParserOptions + +instance FromHttpApiData (AssetLocation Absolute) where + parseUrlPiece = parseHeader . T.encodeUtf8 + parseHeader = + bimap (T.pack . show) AssetLocation + . parseURI strictURIParserOptions + +instance S.ToParamSchema (AssetLocation r) where + toParamSchema _ = + mempty + & S.type_ ?~ S.SwaggerString + & S.format ?~ "url" + +instance AsHeaders '[AssetLocation r] Asset (Asset, AssetLocation r) where + toHeaders (asset, loc) = (I loc :* Nil, asset) + fromHeaders (I loc :* Nil, asset) = (asset, loc) + +-- | An asset as returned by the download API: if the asset is local, only a +-- URL is returned, and if it is remote the content of the asset is streamed. +data LocalOrRemoteAsset + = LocalAsset (AssetLocation Absolute) + | RemoteAsset (SourceIO ByteString) + +instance + ( ResponseType r0 ~ ErrorDescription code label desc, + ResponseType r1 ~ AssetLocation Absolute, + ResponseType r2 ~ SourceIO ByteString, + KnownSymbol desc + ) => + AsUnion '[r0, r1, r2] (Maybe LocalOrRemoteAsset) + where + toUnion Nothing = Z (I mkErrorDescription) + toUnion (Just (LocalAsset loc)) = S (Z (I loc)) + toUnion (Just (RemoteAsset asset)) = S (S (Z (I asset))) + + fromUnion (Z (I _)) = Nothing + fromUnion (S (Z (I loc))) = Just (LocalAsset loc) + fromUnion (S (S (Z (I asset)))) = Just (RemoteAsset asset) + fromUnion (S (S (S x))) = case x of + +makeLenses ''Asset' +makeLenses ''AssetSettings diff --git a/libs/wire-api/src/Wire/API/Asset/V3.hs b/libs/wire-api/src/Wire/API/Asset/V3.hs deleted file mode 100644 index 7890b020ab..0000000000 --- a/libs/wire-api/src/Wire/API/Asset/V3.hs +++ /dev/null @@ -1,341 +0,0 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE StrictData #-} -{-# LANGUAGE TemplateHaskell #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2020 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.Asset.V3 - ( -- * Asset - Asset, - mkAsset, - assetKey, - assetExpires, - assetToken, - - -- * AssetKey - AssetKey (..), - - -- * AssetToken - AssetToken (..), - NewAssetToken (..), - - -- * Body Construction - buildMultipartBody, - beginMultipartBody, - endMultipartBody, - - -- * AssetHeaders - AssetHeaders (..), - mkHeaders, - - -- * AssetSettings - AssetSettings, - defAssetSettings, - setAssetPublic, - setAssetRetention, - AssetRetention (..), - assetRetentionSeconds, - assetExpiringSeconds, - assetVolatileSeconds, - retentionToTextRep, - ) -where - -import qualified Codec.MIME.Type as MIME -import Control.Lens (makeLenses, (?~)) -import Data.Aeson (FromJSON (..), ToJSON (..)) -import qualified Data.Aeson as Aeson -import Data.Attoparsec.ByteString.Char8 -import Data.Bifunctor -import Data.ByteString.Builder -import Data.ByteString.Conversion -import qualified Data.ByteString.Lazy as LBS -import Data.Id -import Data.Json.Util (UTCTimeMillis (fromUTCTimeMillis), toUTCTimeMillis) -import Data.Proxy -import Data.Schema -import qualified Data.Swagger as S -import qualified Data.Text as T -import Data.Text.Ascii (AsciiBase64Url) -import qualified Data.Text.Encoding as T -import Data.Time.Clock -import qualified Data.UUID as UUID -import Imports -import Servant -import Wire.API.Arbitrary (Arbitrary (..), GenericUniform (..)) - --------------------------------------------------------------------------------- --- Asset - --- | A newly uploaded asset. -data Asset = Asset - { _assetKey :: AssetKey, - _assetExpires :: Maybe UTCTime, - _assetToken :: Maybe AssetToken - } - deriving stock (Eq, Show, Generic) - deriving (FromJSON, ToJSON, S.ToSchema) via Schema Asset - --- Generate expiry time with millisecond precision -instance Arbitrary Asset where - arbitrary = Asset <$> arbitrary <*> (fmap milli <$> arbitrary) <*> arbitrary - where - milli = fromUTCTimeMillis . toUTCTimeMillis - -mkAsset :: AssetKey -> Asset -mkAsset k = Asset k Nothing Nothing - -instance ToSchema Asset where - schema = - object "Asset" $ - Asset - <$> _assetKey .= field "key" schema - <*> (fmap toUTCTimeMillis . _assetExpires) - .= maybe_ - (optField "expires" (fromUTCTimeMillis <$> schema)) - <*> _assetToken .= maybe_ (optField "token" schema) - --------------------------------------------------------------------------------- --- AssetKey - --- | A unique, versioned asset identifier. --- Note: Can be turned into a sum type with additional constructors --- for future versions. -data AssetKey = AssetKeyV3 AssetId AssetRetention - deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform AssetKey) - deriving (FromJSON, ToJSON, S.ToSchema) via (Schema AssetKey) - -instance FromByteString AssetKey where - parser = do - v <- decimal - _ <- char '-' - case (v :: Word) of - 3 -> parseV3 - _ -> fail $ "Invalid asset version: " ++ show v - where - -- AssetKeyV3 ::= Retention "-" uuid - -- Retention ::= decimal - parseV3 = do - r <- parser - _ <- char '-' - b <- takeByteString - case UUID.fromASCIIBytes b of - Just i -> return $! AssetKeyV3 (Id i) r - Nothing -> fail "Invalid asset ID" - -instance ToByteString AssetKey where - builder (AssetKeyV3 i r) = - builder '3' - <> builder '-' - <> builder r - <> builder '-' - <> builder (UUID.toASCIIBytes (toUUID i)) - -instance ToSchema AssetKey where - schema = - (T.decodeUtf8 . toByteString') - .= parsedText "AssetKey" (runParser parser . T.encodeUtf8) - & doc' . S.schema . S.example ?~ toJSON ("3-1-47de4580-ae51-4650-acbb-d10c028cb0ac" :: Text) - -instance S.ToParamSchema AssetKey where - toParamSchema _ = S.toParamSchema (Proxy @Text) - -instance FromHttpApiData AssetKey where - parseUrlPiece = first T.pack . runParser parser . T.encodeUtf8 - --------------------------------------------------------------------------------- --- AssetToken - --- | Asset tokens are bearer tokens that grant access to a single asset. -newtype AssetToken = AssetToken {assetTokenAscii :: AsciiBase64Url} - deriving stock (Eq, Show) - deriving newtype (FromByteString, ToByteString, Arbitrary) - deriving (FromJSON, ToJSON) via (Schema AssetToken) - -instance ToSchema AssetToken where - schema = - AssetToken <$> assetTokenAscii - .= schema - & doc' . S.schema . S.example ?~ toJSON ("aGVsbG8" :: Text) - -instance S.ToParamSchema AssetToken where - toParamSchema _ = S.toParamSchema (Proxy @Text) - -instance FromHttpApiData AssetToken where - parseUrlPiece = first T.pack . runParser parser . T.encodeUtf8 - --- | A newly (re)generated token for an existing asset. -newtype NewAssetToken = NewAssetToken - {newAssetToken :: AssetToken} - deriving stock (Eq, Show) - deriving newtype (Arbitrary) - deriving (FromJSON, ToJSON, S.ToSchema) via (Schema NewAssetToken) - -instance ToSchema NewAssetToken where - schema = - object "NewAssetToken" $ - NewAssetToken <$> newAssetToken .= field "token" schema - --------------------------------------------------------------------------------- --- Body Construction - --- | Build a complete @multipart/mixed@ request body for a one-shot, --- non-resumable asset upload. -buildMultipartBody :: AssetSettings -> MIME.Type -> LByteString -> Builder -buildMultipartBody sets typ bs = - let hdrs = mkHeaders typ bs - in beginMultipartBody sets hdrs <> lazyByteString bs <> endMultipartBody - --- | Begin building a @multipart/mixed@ request body for a non-resumable upload. --- The returned 'Builder' can be immediately followed by the actual asset bytes. -beginMultipartBody :: AssetSettings -> AssetHeaders -> Builder -beginMultipartBody sets (AssetHeaders t l) = - byteString - "--frontier\r\n\ - \Content-Type: application/json\r\n\ - \Content-Length: " - <> int64Dec (LBS.length settingsJson) - <> byteString - "\r\n\ - \\r\n" - <> lazyByteString settingsJson - <> byteString - "\r\n\ - \--frontier\r\n\ - \Content-Type: " - <> byteString (T.encodeUtf8 (MIME.showType t)) - <> byteString - "\r\n\ - \Content-Length: " - <> wordDec l - <> "\r\n\ - \\r\n" - where - settingsJson = Aeson.encode (schemaToJSON sets) - --- | The trailer of a non-resumable @multipart/mixed@ request body initiated --- via 'beginMultipartBody'. -endMultipartBody :: Builder -endMultipartBody = byteString "\r\n--frontier--\r\n" - --------------------------------------------------------------------------------- --- AssetHeaders - --- | Headers provided during upload. -data AssetHeaders = AssetHeaders - { hdrType :: MIME.Type, - hdrLength :: Word - } - -mkHeaders :: MIME.Type -> LByteString -> AssetHeaders -mkHeaders t b = AssetHeaders t (fromIntegral (LBS.length b)) - --------------------------------------------------------------------------------- --- AssetSettings - --- | Settings provided during upload. -data AssetSettings = AssetSettings - { _setAssetPublic :: Bool, - _setAssetRetention :: Maybe AssetRetention - } - deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform AssetSettings) - deriving (FromJSON, ToJSON, S.ToSchema) via (Schema AssetSettings) - -defAssetSettings :: AssetSettings -defAssetSettings = AssetSettings False Nothing - -instance ToSchema AssetSettings where - schema = - object "AssetSettings" $ - AssetSettings - <$> _setAssetPublic .= (fromMaybe False <$> optField "public" schema) - <*> _setAssetRetention .= maybe_ (optField "retention" schema) - --------------------------------------------------------------------------------- --- AssetRetention - --- | The desired asset retention. -data AssetRetention - = -- | The asset is retained indefinitely. Typically used - -- for profile pictures / assets frequently accessed. - AssetEternal - | -- | DEPRECATED: should not be used by clients for new assets - -- The asset is retained indefinitely. - AssetPersistent - | -- | The asset is retained for a short period of time. - AssetVolatile - | -- | The asset is retained indefinitely, storage is optimised - -- for infrequent access - AssetEternalInfrequentAccess - | -- | The asset is retained for an extended period of time, - -- but not indefinitely. - AssetExpiring - deriving stock (Eq, Show, Enum, Bounded, Generic) - deriving (Arbitrary) via (GenericUniform AssetRetention) - deriving (FromJSON, ToJSON, S.ToSchema) via (Schema AssetRetention) - --- | The minimum TTL in seconds corresponding to a chosen retention. -assetRetentionSeconds :: AssetRetention -> Maybe NominalDiffTime -assetRetentionSeconds AssetEternal = Nothing -assetRetentionSeconds AssetPersistent = Nothing -assetRetentionSeconds AssetVolatile = Just assetVolatileSeconds -assetRetentionSeconds AssetEternalInfrequentAccess = Nothing -assetRetentionSeconds AssetExpiring = Just assetExpiringSeconds - -assetVolatileSeconds :: NominalDiffTime -assetVolatileSeconds = 28 * 24 * 3600 -- 28 days - -assetExpiringSeconds :: NominalDiffTime -assetExpiringSeconds = 365 * 24 * 3600 -- 365 days - -instance ToByteString AssetRetention where - builder AssetEternal = builder '1' - builder AssetPersistent = builder '2' - builder AssetVolatile = builder '3' - builder AssetEternalInfrequentAccess = builder '4' - builder AssetExpiring = builder '5' - --- | ByteString representation is used in AssetKey -instance FromByteString AssetRetention where - parser = - decimal >>= \d -> case (d :: Word) of - 1 -> return AssetEternal - 2 -> return AssetPersistent - 3 -> return AssetVolatile - 4 -> return AssetEternalInfrequentAccess - 5 -> return AssetExpiring - _ -> fail $ "Invalid asset retention: " ++ show d - -retentionToTextRep :: AssetRetention -> Text -retentionToTextRep AssetEternal = "eternal" -retentionToTextRep AssetPersistent = "persistent" -retentionToTextRep AssetVolatile = "volatile" -retentionToTextRep AssetEternalInfrequentAccess = "eternal-infrequent_access" -retentionToTextRep AssetExpiring = "expiring" - -instance ToSchema AssetRetention where - schema = - enum @Text "AssetRetention" $ - foldMap - (\value -> element (retentionToTextRep value) value) - [minBound .. maxBound] - -makeLenses ''Asset -makeLenses ''AssetSettings diff --git a/libs/wire-api/src/Wire/API/ErrorDescription.hs b/libs/wire-api/src/Wire/API/ErrorDescription.hs index 3b3b194ca0..30f7ebf71b 100644 --- a/libs/wire-api/src/Wire/API/ErrorDescription.hs +++ b/libs/wire-api/src/Wire/API/ErrorDescription.hs @@ -2,7 +2,6 @@ module Wire.API.ErrorDescription where import Control.Lens (at, (%~), (.~), (<>~), (?~)) import qualified Data.Aeson as A -import qualified Data.ByteString.Lazy as LBS import Data.Metrics.Servant import Data.SOP (I (..), NP (..), NS (..)) import Data.Schema @@ -13,10 +12,12 @@ import qualified Data.Text as Text import GHC.TypeLits (KnownSymbol, Symbol, natVal, symbolVal) import GHC.TypeNats (Nat) import Imports hiding (head) +import Network.HTTP.Types as HTTP import Servant hiding (Handler, addHeader, contentType, respond) import Servant.API (contentType) import Servant.API.ContentTypes (AllMimeRender, AllMimeUnrender) import Servant.API.Status (KnownStatus, statusVal) +import Servant.Client.Core import Servant.Swagger.Internal import Wire.API.Routes.MultiVerb @@ -113,6 +114,7 @@ instance IsResponse cs (ErrorDescription s label desc) where type ResponseStatus (ErrorDescription s label desc) = s + type ResponseBody (ErrorDescription s label desc) = LByteString responseRender = responseRender @cs @(RespondWithErrorDescription s label desc) responseUnrender = responseUnrender @cs @(RespondWithErrorDescription s label desc) @@ -160,18 +162,20 @@ instance IsResponse cs (EmptyErrorForLegacyReasons s desc) where type ResponseStatus (EmptyErrorForLegacyReasons s desc) = s + type ResponseBody (EmptyErrorForLegacyReasons s desc) = () responseRender _ () = pure $ - roAddContentType + addContentType (contentType (Proxy @PlainText)) - (RenderOutput (statusVal (Proxy @s)) mempty mempty) - - responseUnrender _ output = - guard - ( LBS.null (roBody output) - && roStatus output == statusVal (Proxy @s) - ) + Response + { responseStatusCode = statusVal (Proxy @s), + responseHeaders = mempty, + responseBody = (), + responseHttpVersion = HTTP.http11 + } + + responseUnrender _ output = guard (responseStatusCode output == statusVal (Proxy @s)) instance (KnownStatus s, KnownSymbol desc) => diff --git a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs index 3c500512c8..0ce00f642e 100644 --- a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs +++ b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs @@ -1,3 +1,6 @@ +{-# LANGUAGE ApplicativeDo #-} +{-# LANGUAGE RecordWildCards #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2021 Wire Swiss GmbH @@ -20,6 +23,7 @@ module Wire.API.Routes.MultiVerb MultiVerb, Respond, RespondEmpty, + RespondStreaming, WithHeaders, DescHeader, AsHeaders (..), @@ -33,16 +37,15 @@ module Wire.API.Routes.MultiVerb IsResponse (..), IsSwaggerResponse (..), combineResponseSwagger, - RenderOutput (..), - roAddContentType, - roResponse, ResponseTypes, IsResponseList (..), + addContentType, ) where import Control.Applicative -import Control.Lens hiding (Context) +import Control.Lens hiding (Context, (<|)) +import Data.ByteString.Builder import qualified Data.ByteString.Lazy as LBS import qualified Data.CaseInsensitive as CI import Data.Containers.ListUtils @@ -51,16 +54,18 @@ import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap import Data.Metrics.Servant import Data.Proxy import Data.SOP +import Data.Sequence (Seq, (<|), pattern (:<|)) import qualified Data.Sequence as Seq import qualified Data.Swagger as S import qualified Data.Swagger.Declare as S import qualified Data.Text as Text import qualified Data.Text.Encoding as Text +import Data.Typeable import GHC.TypeLits import Generics.SOP as GSOP import Imports import qualified Network.HTTP.Media as M -import Network.HTTP.Types (HeaderName, hContentType) +import Network.HTTP.Types (hContentType) import qualified Network.HTTP.Types as HTTP import Network.HTTP.Types.Status import qualified Network.Wai as Wai @@ -73,6 +78,7 @@ 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) @@ -86,11 +92,11 @@ data Respond (s :: Nat) (desc :: Symbol) (a :: *) -- Includes status code and description. data RespondEmpty (s :: Nat) (desc :: Symbol) -data RenderOutput = RenderOutput - { roStatus :: Status, - roBody :: LByteString, - roHeaders :: [(HeaderName, ByteString)] - } +-- | A type to describe a streaming 'MultiVerb' response. +-- +-- Includes status code, description, framing strategy and content type. Note +-- that the handler return type is hardcoded to be 'SourceIO ByteString'. +data RespondStreaming (s :: Nat) (desc :: Symbol) (framing :: *) (ct :: *) -- | The result of parsing a response as a union alternative of type 'a'. -- @@ -132,16 +138,18 @@ class IsSwaggerResponse a where type family ResponseType a :: * -class IsResponse cs a where +class IsWaiBody (ResponseBody a) => IsResponse cs a where type ResponseStatus a :: Nat + type ResponseBody a :: * - responseRender :: AcceptHeader -> ResponseType a -> Maybe RenderOutput - responseUnrender :: M.MediaType -> RenderOutput -> UnrenderResult (ResponseType a) + responseRender :: AcceptHeader -> ResponseType a -> Maybe (ResponseF (ResponseBody a)) + responseUnrender :: M.MediaType -> ResponseF (ResponseBody a) -> UnrenderResult (ResponseType a) type instance ResponseType (Respond s desc a) = a instance (AllMimeRender cs a, AllMimeUnrender cs a, KnownStatus s) => IsResponse cs (Respond s desc a) where type ResponseStatus (Respond s desc a) = s + type ResponseBody (Respond s desc a) = LByteString -- Note: here it seems like we are rendering for all possible content types, -- only to choose the correct one afterwards. However, render results besides the @@ -150,21 +158,22 @@ instance (AllMimeRender cs a, AllMimeUnrender cs a, KnownStatus s) => IsResponse responseRender (AcceptHeader acc) x = M.mapAcceptMedia (map (uncurry mkRenderOutput) (allMimeRender (Proxy @cs) x)) acc where - mkRenderOutput :: M.MediaType -> LByteString -> (M.MediaType, RenderOutput) + mkRenderOutput :: M.MediaType -> LByteString -> (M.MediaType, Response) mkRenderOutput c body = - (c,) . roAddContentType c $ - RenderOutput - { roStatus = statusVal (Proxy @s), - roBody = body, - roHeaders = [] + (c,) . addContentType c $ + Response + { responseStatusCode = statusVal (Proxy @s), + responseBody = body, + responseHeaders = mempty, + responseHttpVersion = HTTP.http11 } responseUnrender c output = do - guard (roStatus output == statusVal (Proxy @s)) + guard (responseStatusCode output == statusVal (Proxy @s)) let results = allMimeUnrender (Proxy @cs) case lookup c results of Nothing -> empty - Just f -> either UnrenderError UnrenderSuccess (f (roBody output)) + Just f -> either UnrenderError UnrenderSuccess (f (responseBody output)) instance (KnownStatus s, KnownSymbol desc, S.ToSchema a) => @@ -181,20 +190,19 @@ type instance ResponseType (RespondEmpty s desc) = () instance KnownStatus s => IsResponse cs (RespondEmpty s desc) where type ResponseStatus (RespondEmpty s desc) = s + type ResponseBody (RespondEmpty s desc) = () responseRender _ _ = - Just - RenderOutput - { roStatus = statusVal (Proxy @s), - roBody = mempty, - roHeaders = [] + Just $ + Response + { responseStatusCode = statusVal (Proxy @s), + responseBody = (), + responseHeaders = mempty, + responseHttpVersion = HTTP.http11 } responseUnrender _ output = - guard - ( roStatus output == statusVal (Proxy @s) - && LBS.null (roBody output) - ) + guard (responseStatusCode output == statusVal (Proxy @s)) instance (KnownStatus s, KnownSymbol desc) => IsSwaggerResponse (RespondEmpty s desc) where responseSwagger = @@ -202,6 +210,33 @@ instance (KnownStatus s, KnownSymbol desc) => IsSwaggerResponse (RespondEmpty s mempty & S.description .~ Text.pack (symbolVal (Proxy @desc)) +type instance ResponseType (RespondStreaming s desc framing ct) = SourceIO ByteString + +instance + (Accept ct, KnownStatus s) => + IsResponse cs (RespondStreaming s desc framing ct) + where + type ResponseStatus (RespondStreaming s desc framing ct) = s + type ResponseBody (RespondStreaming s desc framing ct) = SourceIO ByteString + responseRender _ x = + pure . addContentType (contentType (Proxy @ct)) $ + Response + { responseStatusCode = statusVal (Proxy @s), + responseBody = x, + responseHeaders = mempty, + responseHttpVersion = HTTP.http11 + } + + responseUnrender _ resp = do + guard (responseStatusCode resp == statusVal (Proxy @s)) + pure $ responseBody resp + +instance (KnownStatus s, KnownSymbol desc) => IsSwaggerResponse (RespondStreaming s desc framing ct) where + responseSwagger = + pure $ + mempty + & S.description .~ Text.pack (symbolVal (Proxy @desc)) + -- | This type adds response headers to a 'MultiVerb' response. -- -- Type variables: @@ -225,7 +260,7 @@ data DescHeader (name :: Symbol) (desc :: Symbol) (a :: *) class ServantHeaders hs xs | hs -> xs where constructHeaders :: NP I xs -> [HTTP.Header] - extractHeaders :: [HTTP.Header] -> Maybe (NP I xs) + extractHeaders :: Seq HTTP.Header -> Maybe (NP I xs) instance ServantHeaders '[] '[] where constructHeaders Nil = [] @@ -251,12 +286,14 @@ instance (headerName @name, toHeader x) : constructHeaders @hs xs + -- FUTUREWORK: should we concatenate all the matching headers instead of just + -- taking the first one? extractHeaders hs = do let name = headerName @name - (hs0, hs1) = partition (\(h, _) -> h == name) hs + (hs0, hs1) = Seq.partition (\(h, _) -> h == name) hs x <- case hs0 of - [] -> empty - ((_, h) : _) -> either (const empty) pure (parseHeader h) + Seq.Empty -> empty + ((_, h) :<| _) -> either (const empty) pure (parseHeader h) xs <- extractHeaders @hs hs1 pure (I x :* xs) @@ -286,15 +323,19 @@ instance IsResponse cs (WithHeaders hs a r) where type ResponseStatus (WithHeaders hs a r) = ResponseStatus r + type ResponseBody (WithHeaders hs a r) = ResponseBody r responseRender acc x = fmap addHeaders $ responseRender @cs @r acc y where (hs, y) = toHeaders @xs x - addHeaders r = r {roHeaders = roHeaders r ++ constructHeaders @hs hs} + addHeaders r = + r + { responseHeaders = responseHeaders r <> Seq.fromList (constructHeaders @hs hs) + } responseUnrender c output = do x <- responseUnrender @cs @r c output - case extractHeaders @hs (roHeaders output) of + case extractHeaders @hs (responseHeaders output) of Nothing -> UnrenderError "Failed to parse headers" Just hs -> pure $ fromHeaders @xs (hs, x) @@ -315,8 +356,8 @@ type family ResponseTypes (as :: [*]) where ResponseTypes (a ': as) = ResponseType a ': ResponseTypes as class IsResponseList cs as where - responseListRender :: AcceptHeader -> Union (ResponseTypes as) -> Maybe RenderOutput - responseListUnrender :: M.MediaType -> RenderOutput -> UnrenderResult (Union (ResponseTypes as)) + responseListRender :: AcceptHeader -> Union (ResponseTypes as) -> Maybe SomeResponse + responseListUnrender :: M.MediaType -> SomeResponse -> UnrenderResult (Union (ResponseTypes as)) responseListStatuses :: [Status] @@ -335,11 +376,11 @@ instance ) => IsResponseList cs (a ': as) where - responseListRender acc (Z (I x)) = responseRender @cs @a acc x + responseListRender acc (Z (I x)) = fmap SomeResponse (responseRender @cs @a acc x) responseListRender acc (S x) = responseListRender @cs @as acc x responseListUnrender c output = - Z . I <$> responseUnrender @cs @a c output + Z . I <$> (responseUnrender @cs @a c =<< fromSomeResponse output) <|> S <$> responseListUnrender @cs @as c output responseListStatuses = statusVal (Proxy @(ResponseStatus a)) : responseListStatuses @cs @as @@ -580,11 +621,56 @@ instance cs = allMime (Proxy @cs) (defs, responses) = S.runDeclare (responseListSwagger @as) mempty -roResponse :: RenderOutput -> Wai.Response -roResponse ro = Wai.responseLBS (roStatus ro) (roHeaders ro) (roBody ro) +class Typeable a => IsWaiBody a where + responseToWai :: ResponseF a -> Wai.Response + +instance IsWaiBody LByteString where + responseToWai r = + Wai.responseLBS + (responseStatusCode r) + (toList (responseHeaders r)) + (responseBody r) + +instance IsWaiBody () where + responseToWai r = + Wai.responseLBS + (responseStatusCode r) + (toList (responseHeaders r)) + mempty + +instance IsWaiBody (SourceIO ByteString) where + responseToWai r = + Wai.responseStream + (responseStatusCode r) + (toList (responseHeaders r)) + $ \output flush -> do + foreach + (const (pure ())) + (\chunk -> output (byteString chunk) *> flush) + (responseBody r) + +data SomeResponse = forall a. IsWaiBody a => SomeResponse (ResponseF a) + +addContentType :: M.MediaType -> ResponseF a -> ResponseF a +addContentType c r = r {responseHeaders = (hContentType, M.renderHeader c) <| responseHeaders r} -roAddContentType :: M.MediaType -> RenderOutput -> RenderOutput -roAddContentType c ro = ro {roHeaders = (hContentType, M.renderHeader c) : roHeaders ro} +setEmptyBody :: SomeResponse -> SomeResponse +setEmptyBody (SomeResponse r) = SomeResponse (go r) + where + go :: ResponseF a -> ResponseF LByteString + go Response {..} = Response {responseBody = mempty, ..} + +someResponseToWai :: SomeResponse -> Wai.Response +someResponseToWai (SomeResponse r) = responseToWai r + +fromSomeResponse :: (Alternative m, Typeable a) => SomeResponse -> m (ResponseF a) +fromSomeResponse (SomeResponse Response {..}) = do + body <- maybe empty pure $ cast responseBody + pure $ + Response + { responseBody = body, + .. + } instance (AllMime cs, IsResponseList cs as, AsUnion as r, ReflectMethod method) => @@ -607,12 +693,11 @@ instance `addAcceptCheck` acceptCheck (Proxy @cs) acc runAction action' env req k $ \output -> do let mresp = responseListRender @cs @as acc (toUnion @as output) - resp' <- case mresp of + someResponseToWai <$> case mresp of Nothing -> FailFatal err406 Just resp - | allowedMethodHead method req -> pure $ resp {roBody = mempty} + | allowedMethodHead method req -> pure (setEmptyBody resp) | otherwise -> pure resp - pure (roResponse resp') where method = reflectMethod (Proxy @method) @@ -647,16 +732,15 @@ instance } c <- getResponseContentType response - let output = - RenderOutput - { roBody = responseBody response, - roHeaders = toList (responseHeaders response), - roStatus = responseStatusCode response - } - unless (any (M.matches c) accept) $ do throwClientError $ UnsupportedContentType c response - case responseListUnrender @cs @as c output of + + -- FUTUREWORK: support streaming + let sresp = + if LBS.null (responseBody response) + then SomeResponse response {responseBody = ()} + else SomeResponse response + case responseListUnrender @cs @as c sresp of StatusMismatch -> throwClientError (DecodeFailure "Status mismatch" response) UnrenderError e -> throwClientError (DecodeFailure (Text.pack e) response) UnrenderSuccess x -> pure (fromUnion @as x) 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 f73dc764a2..f08fdf1c3c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2021 Wire Swiss GmbH @@ -28,11 +26,13 @@ import Imports import Servant import Servant.Swagger.Internal import Servant.Swagger.Internal.Orphans () +import URI.ByteString import Wire.API.Asset import Wire.API.ErrorDescription import Wire.API.Routes.AssetBody import Wire.API.Routes.MultiVerb import Wire.API.Routes.Public +import Wire.API.Routes.QualifiedCapture data PrincipalTag = UserPrincipalTag | BotPrincipalTag | ProviderPrincipalTag deriving (Eq, Show) @@ -61,28 +61,28 @@ instance HasServer (ApplyPrincipalPath tag api) ctx => HasServer (tag :> api) ct instance RoutesToPaths (ApplyPrincipalPath tag api) => RoutesToPaths (tag :> api) where getRoutes = getRoutes @(ApplyPrincipalPath tag api) -newtype AssetLocation = AssetLocation {getAssetLocation :: Text} - deriving newtype - ( ToHttpApiData, - FromHttpApiData, - Swagger.ToParamSchema - ) +type AssetLocationHeader r = + '[DescHeader "Location" "Asset location" (AssetLocation r)] + +type AssetRedirect = + WithHeaders + (AssetLocationHeader Absolute) + (AssetLocation Absolute) + (RespondEmpty 302 "Asset found") -instance AsHeaders '[AssetLocation] Asset (Asset, AssetLocation) where - toHeaders (asset, loc) = (I loc :* Nil, asset) - fromHeaders (I loc :* Nil, asset) = (asset, loc) +type AssetStreaming = + RespondStreaming + 200 + "Asset returned directly with content type `application/octet-stream`" + NoFraming + OctetStream type GetAsset = MultiVerb 'GET '[JSON] - '[ AssetNotFound, - WithHeaders - '[DescHeader "Location" "Asset location" AssetLocation] - AssetLocation - (RespondEmpty 302 "Asset found") - ] - (Maybe AssetLocation) + '[AssetNotFound, AssetRedirect] + (Maybe (AssetLocation Absolute)) type ServantAPI = ( Summary "Renew an asset token" @@ -108,13 +108,14 @@ type ServantAPI = '[RespondEmpty 200 "Asset token deleted"] () ) - :<|> BaseAPI 'UserPrincipalTag - :<|> BaseAPI 'BotPrincipalTag - :<|> BaseAPI 'ProviderPrincipalTag + :<|> BaseAPIv3 'UserPrincipalTag + :<|> BaseAPIv3 'BotPrincipalTag + :<|> BaseAPIv3 'ProviderPrincipalTag + :<|> QualifiedAPI :<|> LegacyAPI :<|> InternalAPI -type BaseAPI (tag :: PrincipalTag) = +type BaseAPIv3 (tag :: PrincipalTag) = ( Summary "Upload an asset" :> CanThrow AssetTooLarge :> CanThrow InvalidLength @@ -124,11 +125,11 @@ type BaseAPI (tag :: PrincipalTag) = 'POST '[JSON] '[ WithHeaders - '[DescHeader "Location" "Asset location" AssetLocation] - (Asset, AssetLocation) + (AssetLocationHeader Relative) + (Asset, AssetLocation Relative) (Respond 201 "Asset posted" Asset) ] - (Asset, AssetLocation) + (Asset, AssetLocation Relative) ) :<|> ( Summary "Download an asset" :> tag @@ -149,6 +150,41 @@ type BaseAPI (tag :: PrincipalTag) = () ) +type QualifiedAPI = + ( Summary "Download an asset" + :> Description + "**Note**: local assets result in a redirect, \ + \while remote assets are streamed directly." + :> ZLocalUser + :> "assets" + :> "v4" + :> QualifiedCapture "key" AssetKey + :> Header "Asset-Token" AssetToken + :> QueryParam "asset_token" AssetToken + :> MultiVerb + 'GET + '[JSON] + '[ AssetNotFound, + AssetRedirect, + AssetStreaming + ] + (Maybe LocalOrRemoteAsset) + ) + :<|> ( Summary "Delete an asset" + :> Description "**Note**: only local assets can be deleted." + :> CanThrow AssetNotFound + :> CanThrow Unauthorised + :> ZLocalUser + :> "assets" + :> "v4" + :> QualifiedCapture "key" AssetKey + :> MultiVerb + 'DELETE + '[JSON] + '[RespondEmpty 200 "Asset deleted"] + () + ) + type LegacyAPI = ( ZLocalUser :> "assets" diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Asset_asset.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Asset_asset.hs index 9e74080103..98da5bf2c1 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Asset_asset.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Asset_asset.hs @@ -19,44 +19,31 @@ module Test.Wire.API.Golden.Generated.Asset_asset where import Control.Lens ((.~)) +import Data.Domain import Data.Id (Id (Id)) +import Data.Qualified import Data.Text.Ascii (AsciiChars (validate)) import qualified Data.UUID as UUID (fromString) import Imports (Functor (fmap), Maybe (Just, Nothing), fromJust, fromRight, read, undefined, (&)) import Wire.API.Asset - ( Asset, - AssetKey (AssetKeyV3), - AssetRetention - ( AssetEternal, - AssetEternalInfrequentAccess, - AssetExpiring, - AssetPersistent, - AssetVolatile - ), - AssetToken (AssetToken, assetTokenAscii), - assetExpires, - assetToken, - mkAsset, - ) testObject_Asset_asset_1 :: Asset testObject_Asset_asset_1 = - ( mkAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "0000004b-0000-0017-0000-003e00000033"))) AssetExpiring) - & assetExpires .~ (fmap read (Just "1864-04-30 15:58:55.452 UTC")) - & assetToken .~ Just (AssetToken {assetTokenAscii = (fromRight undefined (validate ("Kun4JaxR6QuASXywDhzx")))}) - ) + mkAsset (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "0000004b-0000-0017-0000-003e00000033"))) AssetExpiring) (Domain "example.com")) + & assetExpires .~ (fmap read (Just "1864-04-30 15:58:55.452 UTC")) + & assetToken .~ Just (AssetToken {assetTokenAscii = (fromRight undefined (validate ("Kun4JaxR6QuASXywDhzx")))}) testObject_Asset_asset_2 :: Asset testObject_Asset_asset_2 = ( mkAsset - (AssetKeyV3 (Id (fromJust (UUID.fromString "00000008-0000-006c-0000-001900000036"))) AssetEternalInfrequentAccess) + (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "00000008-0000-006c-0000-001900000036"))) AssetEternalInfrequentAccess) (Domain "example.com")) & assetExpires .~ (fmap read (Just "1864-06-04 17:39:43.924 UTC")) & assetToken .~ Just (AssetToken {assetTokenAscii = (fromRight undefined (validate ("mPuul678vuJVZ_u9lQ==")))}) ) testObject_Asset_asset_3 :: Asset testObject_Asset_asset_3 = - ( mkAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "00000055-0000-0071-0000-002e00000020"))) AssetEternal) + ( mkAsset (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "00000055-0000-0071-0000-002e00000020"))) AssetEternal) (Domain "example.com")) & assetExpires .~ (fmap read (Just "1864-05-18 20:18:13.438 UTC")) & assetToken .~ Nothing ) @@ -64,49 +51,49 @@ testObject_Asset_asset_3 = testObject_Asset_asset_4 :: Asset testObject_Asset_asset_4 = ( mkAsset - (AssetKeyV3 (Id (fromJust (UUID.fromString "00000063-0000-0044-0000-003000000059"))) AssetEternalInfrequentAccess) + (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "00000063-0000-0044-0000-003000000059"))) AssetEternalInfrequentAccess) (Domain "example.com")) & assetExpires .~ (fmap read (Nothing)) & assetToken .~ Just (AssetToken {assetTokenAscii = (fromRight undefined (validate ("IRKruiPSiANiX1fL")))}) ) testObject_Asset_asset_5 :: Asset testObject_Asset_asset_5 = - ( mkAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "00000019-0000-005b-0000-001d00000056"))) AssetVolatile) + ( mkAsset (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "00000019-0000-005b-0000-001d00000056"))) AssetVolatile) (Domain "example.com")) & assetExpires .~ (fmap read (Just "1864-05-11 14:38:25.874 UTC")) & assetToken .~ Just (AssetToken {assetTokenAscii = (fromRight undefined (validate ("BrbiaM1RxJlqjlqq7quuPSc=")))}) ) testObject_Asset_asset_6 :: Asset testObject_Asset_asset_6 = - ( mkAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "0000000e-0000-0046-0000-00560000005e"))) AssetPersistent) + ( mkAsset (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "0000000e-0000-0046-0000-00560000005e"))) AssetPersistent) (Domain "example.com")) & assetExpires .~ (fmap read (Just "1864-05-25 01:19:16.676 UTC")) & assetToken .~ Nothing ) testObject_Asset_asset_7 :: Asset testObject_Asset_asset_7 = - ( mkAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "00000013-0000-002e-0000-003000000042"))) AssetEternal) + ( mkAsset (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "00000013-0000-002e-0000-003000000042"))) AssetEternal) (Domain "example.com")) & assetExpires .~ (fmap read (Just "1864-04-14 08:45:43.05 UTC")) & assetToken .~ Just (AssetToken {assetTokenAscii = (fromRight undefined (validate ("_N9ERJGmbZtd6XlW_6O12bxuNe4=")))}) ) testObject_Asset_asset_8 :: Asset testObject_Asset_asset_8 = - ( mkAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "00000073-0000-003e-0000-00120000000c"))) AssetEternal) + ( mkAsset (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "00000073-0000-003e-0000-00120000000c"))) AssetEternal) (Domain "example.com")) & assetExpires .~ (fmap read (Nothing)) & assetToken .~ Nothing ) testObject_Asset_asset_9 :: Asset testObject_Asset_asset_9 = - ( mkAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "00000006-0000-004b-0000-004f00000025"))) AssetPersistent) + ( mkAsset (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "00000006-0000-004b-0000-004f00000025"))) AssetPersistent) (Domain "example.com")) & assetExpires .~ (fmap read (Just "1864-05-21 01:34:09.726 UTC")) & assetToken .~ Nothing ) testObject_Asset_asset_10 :: Asset testObject_Asset_asset_10 = - ( mkAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "00000065-0000-0080-0000-003400000061"))) AssetEternal) + ( mkAsset (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "00000065-0000-0080-0000-003400000061"))) AssetEternal) (Domain "example.com")) & assetExpires .~ (fmap read (Nothing)) & assetToken .~ Nothing ) @@ -114,14 +101,14 @@ testObject_Asset_asset_10 = testObject_Asset_asset_11 :: Asset testObject_Asset_asset_11 = ( mkAsset - (AssetKeyV3 (Id (fromJust (UUID.fromString "00000014-0000-0077-0000-001e00000076"))) AssetEternalInfrequentAccess) + (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "00000014-0000-0077-0000-001e00000076"))) AssetEternalInfrequentAccess) (Domain "example.com")) & assetExpires .~ (fmap read (Just "1864-05-11 16:58:59.746 UTC")) & assetToken .~ Just (AssetToken {assetTokenAscii = (fromRight undefined (validate ("DnlRW9Q=")))}) ) testObject_Asset_asset_12 :: Asset testObject_Asset_asset_12 = - ( mkAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "0000001d-0000-0076-0000-003800000021"))) AssetPersistent) + ( mkAsset (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "0000001d-0000-0076-0000-003800000021"))) AssetPersistent) (Domain "example.com")) & assetExpires .~ (fmap read (Nothing)) & assetToken .~ Nothing ) @@ -129,7 +116,7 @@ testObject_Asset_asset_12 = testObject_Asset_asset_13 :: Asset testObject_Asset_asset_13 = ( mkAsset - (AssetKeyV3 (Id (fromJust (UUID.fromString "00000030-0000-0036-0000-003c0000000a"))) AssetEternalInfrequentAccess) + (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "00000030-0000-0036-0000-003c0000000a"))) AssetEternalInfrequentAccess) (Domain "example.com")) & assetExpires .~ (fmap read (Just "1864-04-30 19:37:57.302 UTC")) & assetToken .~ Just (AssetToken {assetTokenAscii = (fromRight undefined (validate ("n7CJBcdOSKznRmOypWXsGfEE0g==")))}) ) @@ -137,42 +124,42 @@ testObject_Asset_asset_13 = testObject_Asset_asset_14 :: Asset testObject_Asset_asset_14 = ( mkAsset - (AssetKeyV3 (Id (fromJust (UUID.fromString "00000047-0000-0012-0000-005500000062"))) AssetEternalInfrequentAccess) + (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "00000047-0000-0012-0000-005500000062"))) AssetEternalInfrequentAccess) (Domain "example.com")) & assetExpires .~ (fmap read (Just "1864-05-06 09:09:55.146 UTC")) & assetToken .~ Just (AssetToken {assetTokenAscii = (fromRight undefined (validate ("LYfUg4qlMjw=")))}) ) testObject_Asset_asset_15 :: Asset testObject_Asset_asset_15 = - ( mkAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "00000030-0000-0074-0000-00660000004c"))) AssetPersistent) + ( mkAsset (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "00000030-0000-0074-0000-00660000004c"))) AssetPersistent) (Domain "example.com")) & assetExpires .~ (fmap read (Nothing)) & assetToken .~ Nothing ) testObject_Asset_asset_16 :: Asset testObject_Asset_asset_16 = - ( mkAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "00000048-0000-0051-0000-005d00000070"))) AssetVolatile) + ( mkAsset (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "00000048-0000-0051-0000-005d00000070"))) AssetVolatile) (Domain "example.com")) & assetExpires .~ (fmap read (Just "1864-05-04 02:19:12.52 UTC")) & assetToken .~ Nothing ) testObject_Asset_asset_17 :: Asset testObject_Asset_asset_17 = - ( mkAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "00000017-0000-000d-0000-00680000003e"))) AssetPersistent) + ( mkAsset (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "00000017-0000-000d-0000-00680000003e"))) AssetPersistent) (Domain "example.com")) & assetExpires .~ (fmap read (Just "1864-04-09 17:00:39.763 UTC")) & assetToken .~ Nothing ) testObject_Asset_asset_18 :: Asset testObject_Asset_asset_18 = - ( mkAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "0000003e-0000-0032-0000-004d00000070"))) AssetEternal) + ( mkAsset (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "0000003e-0000-0032-0000-004d00000070"))) AssetEternal) (Domain "example.com")) & assetExpires .~ (fmap read (Just "1864-04-12 20:53:21.25 UTC")) & assetToken .~ Nothing ) testObject_Asset_asset_19 :: Asset testObject_Asset_asset_19 = - ( mkAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "00000021-0000-0062-0000-002a0000006b"))) AssetVolatile) + ( mkAsset (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "00000021-0000-0062-0000-002a0000006b"))) AssetVolatile) (Domain "example.com")) & assetExpires .~ (fmap read (Nothing)) & assetToken @@ -181,7 +168,7 @@ testObject_Asset_asset_19 = testObject_Asset_asset_20 :: Asset testObject_Asset_asset_20 = - ( mkAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "00000053-0000-0072-0000-001700000047"))) AssetVolatile) + ( mkAsset (Qualified (AssetKeyV3 (Id (fromJust (UUID.fromString "00000053-0000-0072-0000-001700000047"))) AssetVolatile) (Domain "example.com")) & assetExpires .~ (fmap read (Just "1864-04-25 16:48:39.986 UTC")) & assetToken .~ Nothing ) diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_1.json b/libs/wire-api/test/golden/testObject_Asset_asset_1.json index 9bef9870da..d4f078bbfb 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_1.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_1.json @@ -1,4 +1,5 @@ { + "domain": "example.com", "expires": "1864-04-30T15:58:55.452Z", "key": "3-5-0000004b-0000-0017-0000-003e00000033", "token": "Kun4JaxR6QuASXywDhzx" diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_10.json b/libs/wire-api/test/golden/testObject_Asset_asset_10.json index 1d25e3b58c..c495e0a86c 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_10.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_10.json @@ -1,3 +1,4 @@ { + "domain": "example.com", "key": "3-1-00000065-0000-0080-0000-003400000061" } diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_11.json b/libs/wire-api/test/golden/testObject_Asset_asset_11.json index d6e74f8e6e..2b6ee63fdd 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_11.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_11.json @@ -1,4 +1,5 @@ { + "domain": "example.com", "expires": "1864-05-11T16:58:59.746Z", "key": "3-4-00000014-0000-0077-0000-001e00000076", "token": "DnlRW9Q=" diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_12.json b/libs/wire-api/test/golden/testObject_Asset_asset_12.json index eda1a4fdd6..85bd8fd266 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_12.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_12.json @@ -1,3 +1,4 @@ { + "domain": "example.com", "key": "3-2-0000001d-0000-0076-0000-003800000021" } diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_13.json b/libs/wire-api/test/golden/testObject_Asset_asset_13.json index 8e07a56197..2ac23c24eb 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_13.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_13.json @@ -1,4 +1,5 @@ { + "domain": "example.com", "expires": "1864-04-30T19:37:57.302Z", "key": "3-4-00000030-0000-0036-0000-003c0000000a", "token": "n7CJBcdOSKznRmOypWXsGfEE0g==" diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_14.json b/libs/wire-api/test/golden/testObject_Asset_asset_14.json index 442e556246..a70a668bc8 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_14.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_14.json @@ -1,4 +1,5 @@ { + "domain": "example.com", "expires": "1864-05-06T09:09:55.146Z", "key": "3-4-00000047-0000-0012-0000-005500000062", "token": "LYfUg4qlMjw=" diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_15.json b/libs/wire-api/test/golden/testObject_Asset_asset_15.json index f49cd13e46..770067088c 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_15.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_15.json @@ -1,3 +1,4 @@ { + "domain": "example.com", "key": "3-2-00000030-0000-0074-0000-00660000004c" } diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_16.json b/libs/wire-api/test/golden/testObject_Asset_asset_16.json index 69e6e5f181..bf6597141c 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_16.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_16.json @@ -1,4 +1,5 @@ { + "domain": "example.com", "expires": "1864-05-04T02:19:12.520Z", "key": "3-3-00000048-0000-0051-0000-005d00000070" } diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_17.json b/libs/wire-api/test/golden/testObject_Asset_asset_17.json index ccb77a2d1c..5a4e7a4811 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_17.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_17.json @@ -1,4 +1,5 @@ { + "domain": "example.com", "expires": "1864-04-09T17:00:39.763Z", "key": "3-2-00000017-0000-000d-0000-00680000003e" } diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_18.json b/libs/wire-api/test/golden/testObject_Asset_asset_18.json index 516f95363f..8f02aeae56 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_18.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_18.json @@ -1,4 +1,5 @@ { + "domain": "example.com", "expires": "1864-04-12T20:53:21.250Z", "key": "3-1-0000003e-0000-0032-0000-004d00000070" } diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_19.json b/libs/wire-api/test/golden/testObject_Asset_asset_19.json index 4b62e85e30..c8ea25d227 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_19.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_19.json @@ -1,4 +1,5 @@ { + "domain": "example.com", "key": "3-3-00000021-0000-0062-0000-002a0000006b", "token": "4wm3D03aqvZ_0oKFtwXCYnSTC7m_z1E=" } diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_2.json b/libs/wire-api/test/golden/testObject_Asset_asset_2.json index 3a8d556bd5..a4e0765c06 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_2.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_2.json @@ -1,4 +1,5 @@ { + "domain": "example.com", "expires": "1864-06-04T17:39:43.924Z", "key": "3-4-00000008-0000-006c-0000-001900000036", "token": "mPuul678vuJVZ_u9lQ==" diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_20.json b/libs/wire-api/test/golden/testObject_Asset_asset_20.json index ee08bfe6d1..3cd958cedc 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_20.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_20.json @@ -1,4 +1,5 @@ { + "domain": "example.com", "expires": "1864-04-25T16:48:39.986Z", "key": "3-3-00000053-0000-0072-0000-001700000047" } diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_3.json b/libs/wire-api/test/golden/testObject_Asset_asset_3.json index eb3537825f..13b90b2b05 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_3.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_3.json @@ -1,4 +1,5 @@ { + "domain": "example.com", "expires": "1864-05-18T20:18:13.438Z", "key": "3-1-00000055-0000-0071-0000-002e00000020" } diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_4.json b/libs/wire-api/test/golden/testObject_Asset_asset_4.json index d43de9f110..fc65d82c3c 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_4.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_4.json @@ -1,4 +1,5 @@ { + "domain": "example.com", "key": "3-4-00000063-0000-0044-0000-003000000059", "token": "IRKruiPSiANiX1fL" } diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_5.json b/libs/wire-api/test/golden/testObject_Asset_asset_5.json index 0bd2857635..37a8a6a8dc 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_5.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_5.json @@ -1,4 +1,5 @@ { + "domain": "example.com", "expires": "1864-05-11T14:38:25.874Z", "key": "3-3-00000019-0000-005b-0000-001d00000056", "token": "BrbiaM1RxJlqjlqq7quuPSc=" diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_6.json b/libs/wire-api/test/golden/testObject_Asset_asset_6.json index 8d9571f2dc..506b8af9ec 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_6.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_6.json @@ -1,4 +1,5 @@ { + "domain": "example.com", "expires": "1864-05-25T01:19:16.676Z", "key": "3-2-0000000e-0000-0046-0000-00560000005e" } diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_7.json b/libs/wire-api/test/golden/testObject_Asset_asset_7.json index b97f270a80..5d9fd890b0 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_7.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_7.json @@ -1,4 +1,5 @@ { + "domain": "example.com", "expires": "1864-04-14T08:45:43.050Z", "key": "3-1-00000013-0000-002e-0000-003000000042", "token": "_N9ERJGmbZtd6XlW_6O12bxuNe4=" diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_8.json b/libs/wire-api/test/golden/testObject_Asset_asset_8.json index 434d93f714..e23c34c543 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_8.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_8.json @@ -1,3 +1,4 @@ { + "domain": "example.com", "key": "3-1-00000073-0000-003e-0000-00120000000c" } diff --git a/libs/wire-api/test/golden/testObject_Asset_asset_9.json b/libs/wire-api/test/golden/testObject_Asset_asset_9.json index 5e7097dcd9..5c33c2d979 100644 --- a/libs/wire-api/test/golden/testObject_Asset_asset_9.json +++ b/libs/wire-api/test/golden/testObject_Asset_asset_9.json @@ -1,4 +1,5 @@ { + "domain": "example.com", "expires": "1864-05-21T01:34:09.726Z", "key": "3-2-00000006-0000-004b-0000-004f00000025" } diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_16.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_16.json deleted file mode 100644 index 156ade504d..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_16.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "email": "%x\u0013􀔑\u0004.@G빯t.6", - "phone": "+298116118047", - "sso_id": { - "subject": "me@example.com", - "tenant": "http://example.com" - } -} diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_5.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_5.json deleted file mode 100644 index 902e47fbe8..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_5.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "email": null, - "phone": "+49198172826", - "sso_id": { - "subject": "me@example.com", - "tenant": "http://example.com" - } -} diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_8.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_8.json deleted file mode 100644 index f9a46004b6..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_8.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "email": null, - "phone": "+149548802116267", - "sso_id": { - "subject": "me@example.com", - "tenant": "http://example.com" - } -} diff --git a/libs/wire-api/test/golden/testObject_UserSSOId_user_1.json b/libs/wire-api/test/golden/testObject_UserSSOId_user_1.json deleted file mode 100644 index 520bcfc7da..0000000000 --- a/libs/wire-api/test/golden/testObject_UserSSOId_user_1.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "subject": "𝢱􁱝S\u0006\\\u0017\\", - "tenant": "#ph􀽌" -} diff --git a/libs/wire-api/test/golden/testObject_UserSSOId_user_10.json b/libs/wire-api/test/golden/testObject_UserSSOId_user_10.json deleted file mode 100644 index 269300657d..0000000000 --- a/libs/wire-api/test/golden/testObject_UserSSOId_user_10.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "subject": "􀞢^}Y7A\u0014󰐺\u001bF", - "tenant": "oo\"u/]5" -} diff --git a/libs/wire-api/test/golden/testObject_UserSSOId_user_11.json b/libs/wire-api/test/golden/testObject_UserSSOId_user_11.json deleted file mode 100644 index 46b703e246..0000000000 --- a/libs/wire-api/test/golden/testObject_UserSSOId_user_11.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "scim_external_id": "U㞠\u00129[𮥂z􆔇ⵍ􎹘#~􀐽D\u0003[􏈫u𦷊h똶㕠2 c4􄯇\u000e" -} diff --git a/libs/wire-api/test/golden/testObject_UserSSOId_user_12.json b/libs/wire-api/test/golden/testObject_UserSSOId_user_12.json deleted file mode 100644 index db68edf1a2..0000000000 --- a/libs/wire-api/test/golden/testObject_UserSSOId_user_12.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "subject": "􏺁\u001bg𑄉", - "tenant": "\na," -} diff --git a/libs/wire-api/test/golden/testObject_UserSSOId_user_14.json b/libs/wire-api/test/golden/testObject_UserSSOId_user_14.json deleted file mode 100644 index 4d74fb56c9..0000000000 --- a/libs/wire-api/test/golden/testObject_UserSSOId_user_14.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "subject": "g\ta\u001d󳹝[a\u0013𢝝oA", - "tenant": "g􉙇)By𡑗h.\u000c\u00179@" -} diff --git a/libs/wire-api/test/golden/testObject_UserSSOId_user_15.json b/libs/wire-api/test/golden/testObject_UserSSOId_user_15.json deleted file mode 100644 index 69528cc516..0000000000 --- a/libs/wire-api/test/golden/testObject_UserSSOId_user_15.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "scim_external_id": "a9qᩤ󶴏nM]vM\u0012t풣_'\u0010t1MJb{󼥁\u001dZC\u0006" -} diff --git a/libs/wire-api/test/golden/testObject_UserSSOId_user_16.json b/libs/wire-api/test/golden/testObject_UserSSOId_user_16.json deleted file mode 100644 index 9b9641de71..0000000000 --- a/libs/wire-api/test/golden/testObject_UserSSOId_user_16.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "scim_external_id": "Ltepz\u0006\u001c\u001c\u0000􇀶󽍉}𡃭N뫴7GJ" -} diff --git a/libs/wire-api/test/golden/testObject_UserSSOId_user_17.json b/libs/wire-api/test/golden/testObject_UserSSOId_user_17.json deleted file mode 100644 index 830c5048c4..0000000000 --- a/libs/wire-api/test/golden/testObject_UserSSOId_user_17.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "scim_external_id": "qj𤂎.^" -} diff --git a/libs/wire-api/test/golden/testObject_UserSSOId_user_18.json b/libs/wire-api/test/golden/testObject_UserSSOId_user_18.json deleted file mode 100644 index 764dfe765c..0000000000 --- a/libs/wire-api/test/golden/testObject_UserSSOId_user_18.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "scim_external_id": "𒍧" -} diff --git a/libs/wire-api/test/golden/testObject_UserSSOId_user_19.json b/libs/wire-api/test/golden/testObject_UserSSOId_user_19.json deleted file mode 100644 index f1874f30cf..0000000000 --- a/libs/wire-api/test/golden/testObject_UserSSOId_user_19.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "scim_external_id": "!𛉋mᅛ\u0018\u001dA\u0010󿃯𤧇x[h\n~􋁝" -} diff --git a/libs/wire-api/test/golden/testObject_UserSSOId_user_20.json b/libs/wire-api/test/golden/testObject_UserSSOId_user_20.json deleted file mode 100644 index 6476075a9d..0000000000 --- a/libs/wire-api/test/golden/testObject_UserSSOId_user_20.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "subject": "\u0002b\u000e􇆽\u001b\u001d3,􅲈𠩀8𑿋", - "tenant": "X#\u0004 " -} diff --git a/libs/wire-api/test/golden/testObject_UserSSOId_user_3.json b/libs/wire-api/test/golden/testObject_UserSSOId_user_3.json deleted file mode 100644 index 1db46e5e7f..0000000000 --- a/libs/wire-api/test/golden/testObject_UserSSOId_user_3.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "subject": "𨊌4X\u0019", - "tenant": "i\\\u0004\r𘑍\u0015󲛚줴Vi" -} diff --git a/libs/wire-api/test/golden/testObject_UserSSOId_user_4.json b/libs/wire-api/test/golden/testObject_UserSSOId_user_4.json deleted file mode 100644 index eb3dcf271d..0000000000 --- a/libs/wire-api/test/golden/testObject_UserSSOId_user_4.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "subject": "􉹡0\u001b𬴯 createAnonUser "anon" brig ast <- uploadAsset cargohold uid "this is my profile pic" -- Ensure that the asset is there - downloadAsset cargohold uid (toByteString' (ast ^. CHV3.assetKey)) !!! const 200 === statusCode - let newAssets = Just [ImageAsset (T.decodeLatin1 $ toByteString' (ast ^. CHV3.assetKey)) (Just AssetComplete)] + downloadAsset cargohold uid (ast ^. Asset.assetKey) !!! const 200 === statusCode + let newAssets = + Just + [ ImageAsset + (T.decodeLatin1 $ toByteString' (qUnqualified (ast ^. Asset.assetKey))) + (Just AssetComplete) + ] userUpdate = UserUpdate Nothing Nothing newAssets Nothing update = RequestBodyLBS . encode $ userUpdate -- Update profile with the uploaded asset @@ -1330,7 +1335,7 @@ testDeleteWithProfilePic brig cargohold = do !!! const 200 === statusCode deleteUser uid Nothing brig !!! const 200 === statusCode -- Check that the asset gets deleted - downloadAsset cargohold uid (toByteString' (ast ^. CHV3.assetKey)) !!! const 404 === statusCode + downloadAsset cargohold uid (ast ^. Asset.assetKey) !!! const 404 === statusCode testDeleteWithRemotes :: Opt.Opts -> Brig -> Http () testDeleteWithRemotes opts brig = do diff --git a/services/brig/test/integration/API/User/Util.hs b/services/brig/test/integration/API/User/Util.hs index 6e8087c496..665a4351e9 100644 --- a/services/brig/test/integration/API/User/Util.hs +++ b/services/brig/test/integration/API/User/Util.hs @@ -53,6 +53,7 @@ import Imports import qualified Test.Tasty.Cannon as WS import Test.Tasty.HUnit import Util +import Wire.API.Asset import qualified Wire.API.Event.Conversation as Conv import qualified Wire.API.Federation.API.Brig as F import Wire.API.Federation.Component @@ -426,11 +427,16 @@ uploadAsset c usr dat = do === statusCode responseJsonError rsp -downloadAsset :: CargoHold -> UserId -> ByteString -> (MonadIO m, MonadHttp m) => m (Response (Maybe LB.ByteString)) +downloadAsset :: + (MonadIO m, MonadHttp m) => + CargoHold -> + UserId -> + Qualified AssetKey -> + m (Response (Maybe LB.ByteString)) downloadAsset c usr ast = get ( c - . paths ["/assets/v3", ast] + . paths ["/assets/v4", toByteString' (qDomain ast), toByteString' (qUnqualified ast)] . zUser usr . zConn "conn" ) diff --git a/services/cargohold/cargohold.cabal b/services/cargohold/cargohold.cabal index 778c25303a..26330a6c3c 100644 --- a/services/cargohold/cargohold.cabal +++ b/services/cargohold/cargohold.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: b8ba2ed196939ab2b035138d3e428c84b88ff77ff219741840e47ae8dac9de0a +-- hash: 3633e0db5e928a51056ef8474859ed8f6edf06bc84b9ade6db39f62c48bfce07 name: cargohold version: 1.5.0 @@ -29,6 +29,7 @@ library CargoHold.API.Federation CargoHold.API.Legacy CargoHold.API.Public + CargoHold.API.Util CargoHold.API.V3 CargoHold.App CargoHold.AWS @@ -133,6 +134,8 @@ executable cargohold executable cargohold-integration main-is: Main.hs other-modules: + API + API.Util API.V3 Metrics TestSetup @@ -172,5 +175,6 @@ executable cargohold-integration , types-common >=0.7 , uuid >=1.3 , wai-utilities >=0.12 + , wire-api , yaml >=0.8 default-language: Haskell2010 diff --git a/services/cargohold/package.yaml b/services/cargohold/package.yaml index d4bd3dfbcd..09cd81ee87 100644 --- a/services/cargohold/package.yaml +++ b/services/cargohold/package.yaml @@ -81,6 +81,7 @@ executables: - types-common >=0.7 - uuid >=1.3 - wai-utilities >=0.12 + - wire-api cargohold: main: src/Main.hs ghc-options: diff --git a/services/cargohold/src/CargoHold/API/Public.hs b/services/cargohold/src/CargoHold/API/Public.hs index 1c588ce99b..81e3d24910 100644 --- a/services/cargohold/src/CargoHold/API/Public.hs +++ b/services/cargohold/src/CargoHold/API/Public.hs @@ -18,16 +18,18 @@ module CargoHold.API.Public (servantSitemap) where import qualified CargoHold.API.Legacy as LegacyAPI +import CargoHold.API.Util import qualified CargoHold.API.V3 as V3 import CargoHold.App import qualified CargoHold.Types.V3 as V3 import Control.Lens -import Data.ByteString.Conversion +import Data.ByteString.Builder +import qualified Data.ByteString.Lazy as LBS +import Data.Domain import Data.Id import Data.Qualified -import qualified Data.Text.Encoding as Text -import qualified Data.Text.Encoding.Error as Text import Imports hiding (head) +import qualified Network.HTTP.Types as HTTP import Servant ((:<|>) (..)) import Servant.Server hiding (Handler) import URI.ByteString @@ -41,19 +43,46 @@ servantSitemap = :<|> userAPI :<|> botAPI :<|> providerAPI + :<|> qualifiedAPI :<|> legacyAPI :<|> internalAPI where - userAPI :: forall tag. tag ~ 'UserPrincipalTag => ServerT (BaseAPI tag) Handler + userAPI :: forall tag. tag ~ 'UserPrincipalTag => ServerT (BaseAPIv3 tag) Handler userAPI = uploadAssetV3 @tag :<|> downloadAssetV3 @tag :<|> deleteAssetV3 @tag - botAPI :: forall tag. tag ~ 'BotPrincipalTag => ServerT (BaseAPI tag) Handler + botAPI :: forall tag. tag ~ 'BotPrincipalTag => ServerT (BaseAPIv3 tag) Handler botAPI = uploadAssetV3 @tag :<|> downloadAssetV3 @tag :<|> deleteAssetV3 @tag - providerAPI :: forall tag. tag ~ 'ProviderPrincipalTag => ServerT (BaseAPI tag) Handler + providerAPI :: forall tag. tag ~ 'ProviderPrincipalTag => ServerT (BaseAPIv3 tag) Handler providerAPI = uploadAssetV3 @tag :<|> downloadAssetV3 @tag :<|> deleteAssetV3 @tag legacyAPI = legacyDownloadPlain :<|> legacyDownloadPlain :<|> legacyDownloadOtr + qualifiedAPI = downloadAssetV4 :<|> deleteAssetV4 internalAPI = pure () -class MakePrincipal (tag :: PrincipalTag) (id :: *) | id -> tag, tag -> id where +class HasLocation (tag :: PrincipalTag) where + assetLocation :: Local AssetKey -> [Text] + +instance HasLocation 'UserPrincipalTag where + assetLocation key = + [ "assets", + "v4", + domainText (tDomain key), + assetKeyToText (tUnqualified key) + ] + +instance HasLocation 'BotPrincipalTag where + assetLocation key = + [ "bot", + "assets", + assetKeyToText (tUnqualified key) + ] + +instance HasLocation 'ProviderPrincipalTag where + assetLocation key = + [ "provider", + "assets", + assetKeyToText (tUnqualified key) + ] + +class HasLocation tag => MakePrincipal (tag :: PrincipalTag) (id :: *) | id -> tag, tag -> id where mkPrincipal :: id -> V3.Principal instance MakePrincipal 'UserPrincipalTag (Local UserId) where @@ -65,20 +94,36 @@ instance MakePrincipal 'BotPrincipalTag BotId where instance MakePrincipal 'ProviderPrincipalTag ProviderId where mkPrincipal = V3.ProviderPrincipal +mkAssetLocation :: + forall (tag :: PrincipalTag). + HasLocation tag => + Local AssetKey -> + AssetLocation Relative +mkAssetLocation key = + AssetLocation + RelativeRef + { rrAuthority = Nothing, + rrPath = path, + rrQuery = mempty, + rrFragment = Nothing + } + where + path = + LBS.toStrict + . toLazyByteString + . HTTP.encodePathSegmentsRelative + $ assetLocation @tag key + uploadAssetV3 :: + forall tag id. MakePrincipal tag id => id -> AssetSource -> - Handler (Asset, AssetLocation) + Handler (Asset, AssetLocation Relative) uploadAssetV3 pid req = do let principal = mkPrincipal pid asset <- V3.upload principal (getAssetSource req) - let key = Text.decodeUtf8With Text.lenientDecode (toByteString' (asset ^. assetKey)) - let loc = case principal of - V3.UserPrincipal {} -> "/assets/v3/" <> key - V3.BotPrincipal {} -> "/bot/assets/" <> key - V3.ProviderPrincipal {} -> "/provider/assets/" <> key - pure (asset, AssetLocation loc) + pure (fmap qUntagged asset, mkAssetLocation @tag (asset ^. assetKey)) downloadAssetV3 :: MakePrincipal tag id => @@ -86,14 +131,28 @@ downloadAssetV3 :: AssetKey -> Maybe AssetToken -> Maybe AssetToken -> - Handler (Maybe AssetLocation) + Handler (Maybe (AssetLocation Absolute)) downloadAssetV3 usr key tok1 tok2 = do - url <- V3.download (mkPrincipal usr) key (tok1 <|> tok2) - pure $ fmap (AssetLocation . Text.decodeUtf8With Text.lenientDecode . serializeURIRef') url + AssetLocation <$$> V3.download (mkPrincipal usr) key (tok1 <|> tok2) + +downloadAssetV4 :: + Local UserId -> + Qualified AssetKey -> + Maybe AssetToken -> + Maybe AssetToken -> + Handler (Maybe LocalOrRemoteAsset) +downloadAssetV4 usr qkey tok1 tok2 = do + key <- tUnqualified <$> ensureLocal qkey + LocalAsset <$$> downloadAssetV3 usr key tok1 tok2 deleteAssetV3 :: MakePrincipal tag id => id -> AssetKey -> Handler () deleteAssetV3 usr key = V3.delete (mkPrincipal usr) key +deleteAssetV4 :: Local UserId -> Qualified AssetKey -> Handler () +deleteAssetV4 usr qkey = do + key <- tUnqualified <$> ensureLocal qkey + V3.delete (mkPrincipal usr) key + renewTokenV3 :: Local UserId -> AssetKey -> Handler NewAssetToken renewTokenV3 (tUnqualified -> usr) key = NewAssetToken <$> V3.renewToken (V3.UserPrincipal usr) key @@ -101,12 +160,10 @@ renewTokenV3 (tUnqualified -> usr) key = deleteTokenV3 :: Local UserId -> AssetKey -> Handler () deleteTokenV3 (tUnqualified -> usr) key = V3.deleteToken (V3.UserPrincipal usr) key -legacyDownloadPlain :: Local UserId -> ConvId -> AssetId -> Handler (Maybe AssetLocation) -legacyDownloadPlain (tUnqualified -> usr) cnv ast = do - url <- LegacyAPI.download usr cnv ast - pure $ fmap (AssetLocation . Text.decodeUtf8With Text.lenientDecode . serializeURIRef') url +legacyDownloadPlain :: Local UserId -> ConvId -> AssetId -> Handler (Maybe (AssetLocation Absolute)) +legacyDownloadPlain (tUnqualified -> usr) cnv ast = + AssetLocation <$$> LegacyAPI.download usr cnv ast -legacyDownloadOtr :: Local UserId -> ConvId -> AssetId -> Handler (Maybe AssetLocation) -legacyDownloadOtr (tUnqualified -> usr) cnv ast = do - url <- LegacyAPI.downloadOtr usr cnv ast - pure $ fmap (AssetLocation . Text.decodeUtf8With Text.lenientDecode . serializeURIRef') url +legacyDownloadOtr :: Local UserId -> ConvId -> AssetId -> Handler (Maybe (AssetLocation Absolute)) +legacyDownloadOtr (tUnqualified -> usr) cnv ast = + AssetLocation <$$> LegacyAPI.downloadOtr usr cnv ast diff --git a/services/cargohold/src/CargoHold/API/Util.hs b/services/cargohold/src/CargoHold/API/Util.hs new file mode 100644 index 0000000000..33c30e480f --- /dev/null +++ b/services/cargohold/src/CargoHold/API/Util.hs @@ -0,0 +1,39 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2021 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 CargoHold.API.Util + ( ensureLocal, + qualifyLocal, + ) +where + +import CargoHold.App +import Control.Error +import Control.Lens +import Data.Qualified +import Imports +import Wire.API.Federation.Error + +ensureLocal :: Qualified a -> Handler (Local a) +ensureLocal value = do + loc <- view localUnit + foldQualified loc pure (\_ -> throwE federationNotImplemented) value + +qualifyLocal :: a -> Handler (Local a) +qualifyLocal x = do + loc <- view localUnit + pure (qualifyAs loc x) diff --git a/services/cargohold/src/CargoHold/API/V3.hs b/services/cargohold/src/CargoHold/API/V3.hs index fcbe55d553..07917d86b0 100644 --- a/services/cargohold/src/CargoHold/API/V3.hs +++ b/services/cargohold/src/CargoHold/API/V3.hs @@ -26,6 +26,7 @@ module CargoHold.API.V3 where import CargoHold.API.Error +import CargoHold.API.Util import CargoHold.App import qualified CargoHold.Metrics as Metrics import CargoHold.Options @@ -48,6 +49,7 @@ import Data.Conduit import qualified Data.Conduit.Attoparsec as Conduit import Data.Id import qualified Data.List as List +import Data.Qualified import qualified Data.Text.Ascii as Ascii import Data.Text.Encoding (decodeLatin1) import qualified Data.Text.Lazy as LT @@ -57,8 +59,9 @@ import Imports hiding (take) import Network.HTTP.Types.Header import Network.Wai.Utilities (Error (..)) import URI.ByteString +import Wire.API.Asset -upload :: V3.Principal -> ConduitM () ByteString (ResourceT IO) () -> Handler V3.Asset +upload :: V3.Principal -> ConduitM () ByteString (ResourceT IO) () -> Handler (Asset' (Local AssetKey)) upload own bdy = do (rsrc, sets) <- parseMetadata bdy assetSettings (src, hdrs) <- parseHeaders rsrc assetHeaders @@ -71,8 +74,8 @@ upload own bdy = do ast <- liftIO $ Id <$> nextRandom tok <- if sets ^. V3.setAssetPublic then return Nothing else Just <$> randToken let ret = fromMaybe V3.AssetPersistent (sets ^. V3.setAssetRetention) - let key = V3.AssetKeyV3 ast ret - void $ S3.uploadV3 own key hdrs tok src + key <- qualifyLocal (V3.AssetKeyV3 ast ret) + void $ S3.uploadV3 own (tUnqualified key) hdrs tok src Metrics.s3UploadOk Metrics.s3UploadSize cl expires <- case V3.assetRetentionSeconds ret of diff --git a/services/cargohold/src/CargoHold/App.hs b/services/cargohold/src/CargoHold/App.hs index 79aa9dddfa..faeeb21b9f 100644 --- a/services/cargohold/src/CargoHold/App.hs +++ b/services/cargohold/src/CargoHold/App.hs @@ -30,6 +30,7 @@ module CargoHold.App appLogger, requestId, settings, + localUnit, -- * App Monad AppT, @@ -56,6 +57,7 @@ import Control.Monad.Trans.Resource (ResourceT, runResourceT, transResourceT) import Data.Default (def) import Data.Metrics.Middleware (Metrics) import qualified Data.Metrics.Middleware as Metrics +import Data.Qualified import Imports hiding (log) import Network.HTTP.Client (ManagerSettings (..), requestHeaders, responseTimeoutMicro) import Network.HTTP.Client.OpenSSL @@ -75,7 +77,8 @@ data Env = Env _appLogger :: Logger, _httpManager :: Manager, _requestId :: RequestId, - _settings :: Opt.Settings + _settings :: Opt.Settings, + _localUnit :: Local () } makeLenses ''Env @@ -86,7 +89,8 @@ newEnv o = do lgr <- Log.mkLogger (o ^. optLogLevel) (o ^. optLogNetStrings) (o ^. optLogFormat) mgr <- initHttpManager (o ^. optAws . awsS3Compatibility) ama <- initAws (o ^. optAws) lgr mgr - return $ Env ama met lgr mgr def (o ^. optSettings) + let loc = toLocalUnsafe (o ^. optSettings . Opt.setFederationDomain) () + return $ Env ama met lgr mgr def (o ^. optSettings) loc initAws :: AWSOpts -> Logger -> Manager -> IO AWS.Env initAws o l m = diff --git a/services/cargohold/test/integration/API.hs b/services/cargohold/test/integration/API.hs new file mode 100644 index 0000000000..51f60a757b --- /dev/null +++ b/services/cargohold/test/integration/API.hs @@ -0,0 +1,233 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2021 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module API (tests) where + +import API.Util +import Bilge hiding (body) +import Bilge.Assert +import qualified CargoHold.Types.V3 as V3 +import qualified Codec.MIME.Type as MIME +import Control.Lens hiding (sets) +import qualified Data.ByteString.Char8 as C8 +import Data.ByteString.Conversion +import Data.Id +import Data.Qualified +import Data.Time.Clock +import Data.Time.Format +import Data.UUID.V4 +import Imports hiding (head) +import Network.HTTP.Client (parseUrlThrow) +import Network.HTTP.Types.Status (status200) +import Network.Wai.Utilities (Error (label)) +import Test.Tasty +import Test.Tasty.HUnit +import TestSetup + +tests :: IO TestSetup -> TestTree +tests s = + testGroup + "API Integration" + [ testGroup + "simple" + [ test s "roundtrip" testSimpleRoundtrip, + test s "tokens" testSimpleTokens, + test s "s3-upstream-closed" testSimpleS3ClosedConnectionReuse, + test s "client-compatibility" testUploadCompatibility + ] + ] + +-------------------------------------------------------------------------------- +-- Simple (single-step) uploads + +testSimpleRoundtrip :: TestSignature () +testSimpleRoundtrip c = do + let def = V3.defAssetSettings + let rets = [minBound ..] + let sets = def : map (\r -> def & V3.setAssetRetention ?~ r) rets + mapM_ simpleRoundtrip sets + where + simpleRoundtrip sets = do + uid <- liftIO $ Id <$> nextRandom + uid2 <- liftIO $ Id <$> nextRandom + -- Initial upload + let bdy = (applicationText, "Hello World") + r1 <- + uploadSimple (c . path "/assets/v3") uid sets bdy + lookup "Date" (responseHeaders r1) + let utc = parseTimeOrError False defaultTimeLocale rfc822DateFormat date :: UTCTime + -- Potentially check for the expires header + when (isJust $ join (V3.assetRetentionSeconds <$> (sets ^. V3.setAssetRetention))) $ do + liftIO $ assertBool "invalid expiration" (Just utc < view V3.assetExpires ast) + -- Lookup with token and download via redirect. + r2 <- + get (c . path loc . zUser uid . header "Asset-Token" (toByteString' tok) . noRedirect) lookup "Date" (responseHeaders r4) + let utc' = parseTimeOrError False defaultTimeLocale rfc822DateFormat date' :: UTCTime + liftIO $ assertBool "bad date" (utc' >= utc) + +testSimpleTokens :: TestSignature () +testSimpleTokens c = do + uid <- liftIO $ Id <$> nextRandom + uid2 <- liftIO $ Id <$> nextRandom + -- Initial upload + let sets = V3.defAssetSettings & set V3.setAssetRetention (Just V3.AssetVolatile) + let bdy = (applicationText, "Hello World") + r1 <- + uploadSimple (c . path "/assets/v3") uid sets bdy + responseJsonMaybe r2 + liftIO $ assertBool "token unchanged" (tok /= tok') + -- Download by owner with new token. + r3 <- + get (c . path loc . zUser uid . header "Asset-Token" (toByteString' tok') . noRedirect) > wait >> go + where + wait = liftIO $ putStrLn "Waiting for S3 idle timeout ..." >> threadDelay 7000000 + go = do + uid <- liftIO $ Id <$> nextRandom + let sets = V3.defAssetSettings & set V3.setAssetRetention (Just V3.AssetVolatile) + let part2 = (MIME.Type (MIME.Text "plain") [], C8.replicate 100000 'c') + uploadSimple (c . path "/assets/v3") uid sets part2 + !!! const 201 === statusCode + +-------------------------------------------------------------------------------- +-- Client compatibility tests + +-- Since the other tests use functions from the server code, it can happen that +-- an API change also changes the requests made here in the tests. +-- This test tries to prevent us from breaking the API without noticing. +-- +-- The body is taken directly from a request made by the web app +-- (just replaced the content with a shorter one and updated the MD5 header). +testUploadCompatibility :: TestSignature () +testUploadCompatibility c = do + uid <- liftIO $ Id <$> nextRandom + -- Initial upload + r1 <- + uploadRaw (c . path "/assets/v3") uid exampleMultipart + +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module API.Util where + +import Bilge hiding (body) +import qualified CargoHold.Types.V3 as V3 +import qualified Codec.MIME.Parse as MIME +import qualified Codec.MIME.Type as MIME +import Data.ByteString.Builder +import Data.ByteString.Conversion +import qualified Data.ByteString.Lazy as Lazy +import Data.Id +import Data.Qualified +import Data.Text.Encoding (decodeLatin1) +import qualified Data.UUID as UUID +import Imports hiding (head) +import Network.HTTP.Types.Header +import Network.HTTP.Types.Method +import TestSetup + +uploadSimple :: + CargoHold -> + UserId -> + V3.AssetSettings -> + (MIME.Type, ByteString) -> + Http (Response (Maybe Lazy.ByteString)) +uploadSimple c usr sets (ct, bs) = + let mp = V3.buildMultipartBody sets ct (Lazy.fromStrict bs) + in uploadRaw c usr (toLazyByteString mp) + +decodeHeaderOrFail :: (HasCallStack, FromByteString a) => HeaderName -> Response b -> a +decodeHeaderOrFail h = + fromMaybe (error $ "decodeHeaderOrFail: missing or invalid header: " ++ show h) + . fromByteString + . getHeader' h + +uploadRaw :: + CargoHold -> + UserId -> + Lazy.ByteString -> + Http (Response (Maybe Lazy.ByteString)) +uploadRaw c usr bs = + post $ + c + . method POST + . zUser usr + . zConn "conn" + . content "multipart/mixed" + . lbytes bs + +getContentType :: Response a -> Maybe MIME.Type +getContentType = MIME.parseContentType . decodeLatin1 . getHeader' "Content-Type" + +applicationText :: MIME.Type +applicationText = MIME.Type (MIME.Application "text") [] + +applicationOctetStream :: MIME.Type +applicationOctetStream = MIME.Type (MIME.Application "octet-stream") [] + +zUser :: UserId -> Request -> Request +zUser = header "Z-User" . UUID.toASCIIBytes . toUUID + +zConn :: ByteString -> Request -> Request +zConn = header "Z-Connection" + +deleteAssetV3 :: CargoHold -> UserId -> Qualified V3.AssetKey -> Http (Response (Maybe Lazy.ByteString)) +deleteAssetV3 c u k = delete $ c . zUser u . paths ["assets", "v3", toByteString' (qUnqualified k)] + +deleteAsset :: CargoHold -> UserId -> Qualified V3.AssetKey -> Http (Response (Maybe Lazy.ByteString)) +deleteAsset c u k = + delete $ + c . zUser u + . paths + [ "assets", + "v4", + toByteString' (qDomain k), + toByteString' (qUnqualified k) + ] diff --git a/services/cargohold/test/integration/API/V3.hs b/services/cargohold/test/integration/API/V3.hs index f3d2a78001..1b4faaabd5 100644 --- a/services/cargohold/test/integration/API/V3.hs +++ b/services/cargohold/test/integration/API/V3.hs @@ -19,31 +19,24 @@ module API.V3 (tests) where +import API.Util import Bilge hiding (body) import Bilge.Assert -import qualified CargoHold.Types.V3 as V3 -import qualified Codec.MIME.Parse as MIME -import qualified Codec.MIME.Type as MIME import Control.Lens hiding (sets) -import Data.ByteString.Builder import qualified Data.ByteString.Char8 as C8 import Data.ByteString.Conversion -import qualified Data.ByteString.Lazy as Lazy import Data.Id -import Data.Text.Encoding (decodeLatin1) +import Data.Qualified import Data.Time.Clock import Data.Time.Format -import qualified Data.UUID as UUID import Data.UUID.V4 import Imports hiding (head) import Network.HTTP.Client (parseUrlThrow) -import Network.HTTP.Types.Header -import Network.HTTP.Types.Method import Network.HTTP.Types.Status (status200) -import Network.Wai.Utilities (Error (label)) import Test.Tasty import Test.Tasty.HUnit import TestSetup +import Wire.API.Asset tests :: IO TestSetup -> TestTree tests s = @@ -51,11 +44,7 @@ tests s = "API Integration v3" [ testGroup "simple" - [ test s "roundtrip" testSimpleRoundtrip, - test s "tokens" testSimpleTokens, - test s "s3-upstream-closed" testSimpleS3ClosedConnectionReuse, - test s "client-compatibility" testUploadCompatibility - ] + [test s "roundtrip using v3 API" testSimpleRoundtrip] ] -------------------------------------------------------------------------------- @@ -63,9 +52,9 @@ tests s = testSimpleRoundtrip :: TestSignature () testSimpleRoundtrip c = do - let def = V3.defAssetSettings + let def = defAssetSettings let rets = [minBound ..] - let sets = def : map (\r -> def & V3.setAssetRetention ?~ r) rets + let sets = def : map (\r -> def & setAssetRetention ?~ r) rets mapM_ simpleRoundtrip sets where simpleRoundtrip sets = do @@ -76,15 +65,16 @@ testSimpleRoundtrip c = do r1 <- uploadSimple (c . path "/assets/v3") uid sets bdy toByteString' (qUnqualified (ast ^. assetKey)) + let Just tok = view assetToken ast -- Check mandatory Date header let Just date = C8.unpack <$> lookup "Date" (responseHeaders r1) let utc = parseTimeOrError False defaultTimeLocale rfc822DateFormat date :: UTCTime -- Potentially check for the expires header - when (isJust $ join (V3.assetRetentionSeconds <$> (sets ^. V3.setAssetRetention))) $ do - liftIO $ assertBool "invalid expiration" (Just utc < view V3.assetExpires ast) + when (isJust $ join (assetRetentionSeconds <$> (sets ^. setAssetRetention))) $ do + liftIO $ assertBool "invalid expiration" (Just utc < view assetExpires ast) -- Lookup with token and download via redirect. r2 <- get (c . path loc . zUser uid . header "Asset-Token" (toByteString' tok) . noRedirect) lookup "Date" (responseHeaders r4) let utc' = parseTimeOrError False defaultTimeLocale rfc822DateFormat date' :: UTCTime liftIO $ assertBool "bad date" (utc' >= utc) - -testSimpleTokens :: TestSignature () -testSimpleTokens c = do - uid <- liftIO $ Id <$> nextRandom - uid2 <- liftIO $ Id <$> nextRandom - -- Initial upload - let sets = V3.defAssetSettings & set V3.setAssetRetention (Just V3.AssetVolatile) - let bdy = (applicationText, "Hello World") - r1 <- - uploadSimple (c . path "/assets/v3") uid sets bdy - responseJsonMaybe r2 - liftIO $ assertBool "token unchanged" (tok /= tok') - -- Download by owner with new token. - r3 <- - get (c . path loc . zUser uid . header "Asset-Token" (toByteString' tok') . noRedirect) > wait >> go - where - wait = liftIO $ putStrLn "Waiting for S3 idle timeout ..." >> threadDelay 7000000 - go = do - uid <- liftIO $ Id <$> nextRandom - let sets = V3.defAssetSettings & set V3.setAssetRetention (Just V3.AssetVolatile) - let part2 = (MIME.Type (MIME.Text "plain") [], C8.replicate 100000 'c') - uploadSimple (c . path "/assets/v3") uid sets part2 - !!! const 201 === statusCode - --------------------------------------------------------------------------------- --- Client compatibility tests - --- Since the other tests use functions from the server code, it can happen that --- an API change also changes the requests made here in the tests. --- This test tries to prevent us from breaking the API without noticing. --- --- The body is taken directly from a request made by the web app --- (just replaced the content with a shorter one and updated the MD5 header). -testUploadCompatibility :: TestSignature () -testUploadCompatibility c = do - uid <- liftIO $ Id <$> nextRandom - -- Initial upload - r1 <- - uploadRaw (c . path "/assets/v3") uid exampleMultipart - - UserId -> - V3.AssetSettings -> - (MIME.Type, ByteString) -> - Http (Response (Maybe Lazy.ByteString)) -uploadSimple c usr sets (ct, bs) = - let mp = V3.buildMultipartBody sets ct (Lazy.fromStrict bs) - in uploadRaw c usr (toLazyByteString mp) - -uploadRaw :: - CargoHold -> - UserId -> - Lazy.ByteString -> - Http (Response (Maybe Lazy.ByteString)) -uploadRaw c usr bs = - post $ - c - . method POST - . zUser usr - . zConn "conn" - . content "multipart/mixed" - . lbytes bs - -deleteAsset :: CargoHold -> UserId -> V3.AssetKey -> Http (Response (Maybe Lazy.ByteString)) -deleteAsset c u k = delete $ c . zUser u . paths ["assets", "v3", toByteString' k] - --- Utilities ------------------------------------------------------------------ - -decodeHeader :: FromByteString a => HeaderName -> Response b -> a -decodeHeader h = - fromMaybe (error $ "decodeHeader: missing or invalid header: " ++ show h) - . fromByteString - . getHeader' h - -getContentType :: Response a -> Maybe MIME.Type -getContentType = MIME.parseContentType . decodeLatin1 . getHeader' "Content-Type" - -applicationText :: MIME.Type -applicationText = MIME.Type (MIME.Application "text") [] - -applicationOctetStream :: MIME.Type -applicationOctetStream = MIME.Type (MIME.Application "octet-stream") [] - -zUser :: UserId -> Request -> Request -zUser = header "Z-User" . UUID.toASCIIBytes . toUUID - -zConn :: ByteString -> Request -> Request -zConn = header "Z-Connection" diff --git a/services/cargohold/test/integration/Main.hs b/services/cargohold/test/integration/Main.hs index 21e1e33610..9301e222c5 100644 --- a/services/cargohold/test/integration/Main.hs +++ b/services/cargohold/test/integration/Main.hs @@ -20,6 +20,7 @@ module Main ) where +import qualified API import qualified API.V3 import Bilge hiding (body, header) import Data.Proxy @@ -81,7 +82,8 @@ main = runTests go go c i = withResource (getOpts c i) releaseOpts $ \opts -> testGroup "Cargohold" - [ API.V3.tests opts, + [ API.tests opts, + API.V3.tests opts, Metrics.tests opts ] getOpts _ i = do diff --git a/tools/api-simulations/lib/src/Network/Wire/Simulations.hs b/tools/api-simulations/lib/src/Network/Wire/Simulations.hs index 7a34d8280b..f933ab9414 100644 --- a/tools/api-simulations/lib/src/Network/Wire/Simulations.hs +++ b/tools/api-simulations/lib/src/Network/Wire/Simulations.hs @@ -176,7 +176,7 @@ instance Serialize AssetInfo where AssetInfo k t <$> get mkAssetMsg :: Asset -> SymmetricKeys -> BotMessage -mkAssetMsg a = BotAssetMessage . AssetInfo (a ^. assetKey) (a ^. assetToken) +mkAssetMsg a = BotAssetMessage . AssetInfo (qUnqualified (a ^. assetKey)) (a ^. assetToken) mkTextMsg :: Text -> BotMessage mkTextMsg = BotTextMessage