diff --git a/Makefile b/Makefile index 7e8e2aa7b29..8f7e39530ff 100644 --- a/Makefile +++ b/Makefile @@ -85,7 +85,7 @@ cabal.project.local: c: treefmt c-fast .PHONY: c -c-fast: +c-fast: cabal build $(WIRE_CABAL_BUILD_OPTIONS) $(package) || ( make clean-hint; false ) ifeq ($(test), 1) ./hack/bin/cabal-run-tests.sh $(package) $(testargs) @@ -173,7 +173,7 @@ lint-all: formatc hlint-check-all lint-common # The extra 'hlint-check-pr' has been witnessed to be necessary due to # some bu in `hlint-inplace-pr`. Details got lost in history. .PHONY: lint-all-shallow -lint-all-shallow: lint-common formatf hlint-inplace-pr hlint-check-pr +lint-all-shallow: lint-common formatf hlint-inplace-pr hlint-check-pr .PHONY: lint-common lint-common: check-local-nix-derivations treefmt-check # weeder (does not work on CI yet) @@ -602,3 +602,8 @@ upload-bombon: --project-version $(HELM_SEMVER) \ --api-key $(DEPENDENCY_TRACK_API_KEY) \ --auto-create + +.PHONY: openapi-validate +openapi-validate: + @echo -e "Make sure you are running the backend in another terminal (make cr)\n" + vacuum lint -a -d -w <(curl http://localhost:8082/v7/api/swagger.json) diff --git a/changelog.d/3-bug-fixes/ses-notifications b/changelog.d/3-bug-fixes/ses-notifications new file mode 100644 index 00000000000..be2735b450d --- /dev/null +++ b/changelog.d/3-bug-fixes/ses-notifications @@ -0,0 +1 @@ +Process bounce and complaint notifications from SES correctly. \ No newline at end of file diff --git a/changelog.d/4-docs/openapi-validation b/changelog.d/4-docs/openapi-validation new file mode 100644 index 00000000000..a70ca12d5e5 --- /dev/null +++ b/changelog.d/4-docs/openapi-validation @@ -0,0 +1 @@ +Fix openapi validation errors diff --git a/libs/types-common/default.nix b/libs/types-common/default.nix index 3b39ec41402..6e45c4c4d3c 100644 --- a/libs/types-common/default.nix +++ b/libs/types-common/default.nix @@ -123,6 +123,7 @@ mkDerivation { bytestring bytestring-conversion cereal + email-validate imports protobuf string-conversions @@ -132,6 +133,7 @@ mkDerivation { text time unordered-containers + utf8-string uuid ]; description = "Shared type definitions"; diff --git a/libs/types-common/src/Data/Mailbox.hs b/libs/types-common/src/Data/Mailbox.hs new file mode 100644 index 00000000000..c9889d051f4 --- /dev/null +++ b/libs/types-common/src/Data/Mailbox.hs @@ -0,0 +1,214 @@ +module Data.Mailbox where + +import Control.Applicative (optional) +import Data.Aeson +import Data.Attoparsec.ByteString (Parser) +import Data.Attoparsec.ByteString qualified as BSParser +import Data.Attoparsec.ByteString.Char8 qualified as Char8Parser +import Data.Char qualified as Char +import Data.Text qualified as Text +import Data.Text.Encoding qualified as Text +import Imports +import Text.Email.Parser + +-- | Mailbox address according to +-- https://www.rfc-editor.org/rfc/rfc5322#section-3.4 +data Mailbox = Mailbox + { name :: Maybe [Text], + address :: EmailAddress + } + deriving (Show, Eq) + +parseMailbox :: ByteString -> Either String Mailbox +parseMailbox = BSParser.parseOnly (mailboxParser <* BSParser.endOfInput) + +instance FromJSON Mailbox where + parseJSON = + withText "Mailbox" $ + either fail pure . parseMailbox . Text.encodeUtf8 + +-- * Internal + +newtype Comment = Comment [CommentContent] + +data CommentContent = CommentChar Char | SubComment Comment + +atextParser :: Parser Char +atextParser = + alpha + <|> num + <|> allowedSpecials + where + alpha = Char8Parser.satisfy (\c -> Char.isAlpha c && Char.isAscii c) + num = Char8Parser.satisfy Char.isNumber + allowedSpecials = + Char8Parser.satisfy $ + -- Make sure the - is the first or the last symbol, otherwise inClass + -- treats it as a signifier of range + Char8Parser.inClass "-!#$%&'*+/=?^_`{|}~" + +wspParser :: Parser Char +wspParser = Char8Parser.satisfy (\c -> c == ' ' || c == '\t') + +crlfParser :: Parser String +crlfParser = do + void $ Char8Parser.string "\r\n" + pure "\r\n" + +fwsParser :: Parser String +fwsParser = + let wspsAndCrlf = do + wsps <- Char8Parser.many' wspParser + crlf <- crlfParser + pure $ wsps <> crlf + notObs = do + mWspsAndCrlf <- optional wspsAndCrlf + wsps <- Char8Parser.many1' wspParser + pure $ fromMaybe "" mWspsAndCrlf <> wsps + in notObs <|> obsFwsParser + +obsFwsParser :: Parser String +obsFwsParser = do + wsps <- Char8Parser.many1' wspParser + crlfWsps <- Char8Parser.many' $ do + crlf <- crlfParser + wspsAfterCrlf <- Char8Parser.many1' wspParser + pure $ crlf <> wspsAfterCrlf + pure $ concat $ wsps : crlfWsps + +ctextParser :: Parser Char +ctextParser = do + let isAllowedChar w = + (w >= 33 && w <= 39) + || (w >= 42 && w <= 91) + || (w >= 93 && w <= 126) + Char8Parser.satisfy (isAllowedChar . Char.ord) <|> obsNoWsCtl + +-- | US-ASCII control characters that do not include the carriage return, line +-- feed, and white space characters +obsNoWsCtl :: Parser Char +obsNoWsCtl = do + Char8Parser.satisfy + ( \(ord -> c) -> + (c >= 1 && c <= 8) + || c == 11 + || c == 12 + || (c >= 14 && c <= 31) + || (c == 127) + ) + +obsCtextParser, obsQtextParser :: Parser Char +obsCtextParser = obsNoWsCtl +obsQtextParser = obsNoWsCtl + +quotedPairParser :: Parser Char +quotedPairParser = do + void $ Char8Parser.char '\\' + vCharParser <|> wspParser + +vCharParser :: Parser Char +vCharParser = + Char8Parser.satisfy (\c -> ord c >= 0x21 && ord c <= 0x7E) + +ccontentParser :: Parser CommentContent +ccontentParser = + fmap CommentChar ctextParser + <|> fmap CommentChar quotedPairParser + <|> fmap SubComment commentParser + +commentParser :: Parser Comment +commentParser = do + _ <- Char8Parser.char '(' + comment <- Char8Parser.many' $ do + _ <- optional fwsParser + ccontentParser + _ <- Char8Parser.char ')' + pure $ Comment comment + +cfwsParser :: Parser [Comment] +cfwsParser = do + let commentWithFws = do + comment <- Char8Parser.many1' $ do + _ <- optional fwsParser + commentParser + _ <- optional fwsParser + pure comment + commentWithFws <|> fmap (const []) fwsParser + +atomParser :: Parser String +atomParser = do + _ <- optional cfwsParser + atom <- Char8Parser.many1' atextParser + _ <- optional cfwsParser + pure atom + +qtextParser :: Parser Char +qtextParser = + let newParser = Char8Parser.satisfy $ \(ord -> c) -> + c == 33 || (c >= 35 && c <= 91) || (c >= 93 && c <= 126) + in newParser <|> obsQtextParser + +qcontentParser :: Parser Char +qcontentParser = qtextParser <|> quotedPairParser + +quotedStringParser :: Parser String +quotedStringParser = do + _ <- optional cfwsParser + _ <- Char8Parser.char '"' + str <- fmap concat . Char8Parser.many' $ do + mLeadingSpace <- optional fwsParser + c <- qcontentParser + pure $ fromMaybe "" mLeadingSpace <> [c] + mTrailingSpace <- optional fwsParser + _ <- Char8Parser.char '"' + pure $ str <> fromMaybe "" mTrailingSpace + +wordParser :: Parser String +wordParser = atomParser <|> quotedStringParser + +-- | The spec says +-- +-- @ +-- phrase = 1*word / obs-phrase +-- @ +-- +-- Here if we tried to write it using '<|>', parising "John Q. Doe" would +-- succeed with a 'many1 wordParser' while having parsed up to "John Q" and the +-- rest of the string will be left for next parsers, which would likely fail. To +-- avoid all that we can use just the obsPhraseParser, which forces the first +-- thing to be a word and then allows for dots and CFWS. +phraseParser :: Parser [String] +phraseParser = obsPhraseParser + +-- | Ignores comments +obsPhraseParser :: Parser [String] +obsPhraseParser = do + w1 <- wordParser + ws <- fmap catMaybes . Char8Parser.many' $ do + fmap Just wordParser + <|> fmap (Just . (: [])) (Char8Parser.char '.') + <|> fmap (const Nothing) cfwsParser + pure $ w1 : ws + +nameParser :: Parser [Text] +nameParser = map Text.pack <$> phraseParser + +-- | Does not implement parsing for obs-angle-addr +angleAddrParser :: Parser EmailAddress +angleAddrParser = do + _ <- optional cfwsParser + _ <- Char8Parser.char '<' + addr <- addrSpec + _ <- Char8Parser.char '>' + _ <- optional cfwsParser + pure addr + +nameAddrParser :: Parser Mailbox +nameAddrParser = + Mailbox + <$> optional nameParser + <*> angleAddrParser + +mailboxParser :: Parser Mailbox +mailboxParser = + nameAddrParser <|> fmap (Mailbox Nothing) addrSpec diff --git a/libs/types-common/src/Data/Range.hs b/libs/types-common/src/Data/Range.hs index c4401541756..b2dd1b60332 100644 --- a/libs/types-common/src/Data/Range.hs +++ b/libs/types-common/src/Data/Range.hs @@ -225,20 +225,20 @@ instance (ToParamSchema a, KnownNat n, KnownNat m) => ToParamSchema (Range n m [ instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m String) where toParamSchema _ = toParamSchema (Proxy @String) - & S.maxLength ?~ fromKnownNat (Proxy @n) - & S.minLength ?~ fromKnownNat (Proxy @m) + & S.minLength ?~ fromKnownNat (Proxy @n) + & S.maxLength ?~ fromKnownNat (Proxy @m) instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m T.Text) where toParamSchema _ = toParamSchema (Proxy @T.Text) - & S.maxLength ?~ fromKnownNat (Proxy @n) - & S.minLength ?~ fromKnownNat (Proxy @m) + & S.minLength ?~ fromKnownNat (Proxy @n) + & S.maxLength ?~ fromKnownNat (Proxy @m) instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m TL.Text) where toParamSchema _ = toParamSchema (Proxy @TL.Text) - & S.maxLength ?~ fromKnownNat (Proxy @n) - & S.minLength ?~ fromKnownNat (Proxy @m) + & S.minLength ?~ fromKnownNat (Proxy @n) + & S.maxLength ?~ fromKnownNat (Proxy @m) instance (KnownNat n, S.ToSchema a, KnownNat m) => S.ToSchema (Range n m a) where declareNamedSchema _ = diff --git a/libs/types-common/test/Main.hs b/libs/types-common/test/Main.hs index 045e49e5e7e..4814492dfd0 100644 --- a/libs/types-common/test/Main.hs +++ b/libs/types-common/test/Main.hs @@ -21,6 +21,7 @@ module Main where import Imports +import Test.Data.Mailbox qualified as Mailbox import Test.Data.PEMKeys qualified as PEMKeys import Test.Domain qualified as Domain import Test.Handle qualified as Handle @@ -39,5 +40,6 @@ main = Domain.tests, Handle.tests, Qualified.tests, - PEMKeys.tests + PEMKeys.tests, + Mailbox.tests ] diff --git a/libs/types-common/test/Test/Data/Mailbox.hs b/libs/types-common/test/Test/Data/Mailbox.hs new file mode 100644 index 00000000000..caa01307f34 --- /dev/null +++ b/libs/types-common/test/Test/Data/Mailbox.hs @@ -0,0 +1,69 @@ +module Test.Data.Mailbox (tests) where + +import Data.ByteString.UTF8 qualified as UTF8BS +import Data.Mailbox +import Imports +import Test.Tasty +import Test.Tasty.HUnit +import Text.Email.Parser + +validAddresses :: [(ByteString, Mailbox)] +validAddresses = + [ ("john@doe.example", Mailbox Nothing $ unsafeEmailAddress "john" "doe.example"), + ("", Mailbox Nothing $ unsafeEmailAddress "john" "doe.example"), + ("John Doe", Mailbox (Just ["John", "Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("John Doe ", Mailbox (Just ["John", "Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("John Q. Doe ", Mailbox (Just ["John", "Q", ".", "Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John Doe\" ", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John Doe\" (My Best Friend) ", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John@Doe.Example\" (My Friend @ Doe) ", Mailbox (Just ["John@Doe.Example"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John Doe\" (My Best Friend) ", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John \\\"The J\\\" Doe\" ", Mailbox (Just ["John \"The J\" Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John not \\tab\" ", Mailbox (Just ["John not tab"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John [Quoted Special]\" ", Mailbox (Just ["John [Quoted Special]"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John \" ", Mailbox (Just ["John "]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John \r\n NewLine\" ", Mailbox (Just ["John \r\n NewLine"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John Doe\" <(local comment)john(local trailing comment)@doe.example>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John Doe\" <(local comment)\"john\"(local trailing comment)@doe.example>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "\"john\"" "doe.example"), + ("\"John Doe\" <\"john@funkylocal\"@doe.example>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "\"john@funkylocal\"" "doe.example"), + ("\"John Doe\" (trailing comments)", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John Doe\" ", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "[funky@domain.example]"), + ("\"John Doe\" ", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "[doe.example]"), + -- This is wrong, but its how the `email-validate` library does it + ("\"John Doe\" <\"john (not comment)\"@doe.example>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "\"john(notcomment)\"" "doe.example") + ] + +invalidAddresses :: [ByteString] +invalidAddresses = + [ "john", + "john@", + "@doe.example", + "\"john@doe.example\"", + "(john@doe.example)", + "\"John UnendingQuote ", + "John [Unquoted Special] ", + " ", + "\"John \n NoCR\" ", + "\"John \r NoLF\" " + ] + +tests :: TestTree +tests = + testGroup "Mailbox" $ + [ testGroup "valid addressses" $ + map + ( \(addr, expected) -> + testCase (UTF8BS.toString addr) $ + Right expected @=? parseMailbox addr + ) + validAddresses, + testGroup "invalid addresses" $ + map + ( \addr -> + testCase (UTF8BS.toString addr) $ + case parseMailbox addr of + Left _ -> pure () + Right mb -> assertFailure $ "Expected to fail parising, but got: " <> show mb + ) + invalidAddresses + ] diff --git a/libs/types-common/types-common.cabal b/libs/types-common/types-common.cabal index 528890fe064..5144c76d5d9 100644 --- a/libs/types-common/types-common.cabal +++ b/libs/types-common/types-common.cabal @@ -12,6 +12,7 @@ license-file: LICENSE build-type: Simple library + -- cabal-fmt: expand src exposed-modules: Data.Code Data.CommaSeparatedList @@ -24,6 +25,7 @@ library Data.Json.Util Data.LegalHold Data.List1 + Data.Mailbox Data.Misc Data.Nonce Data.PEMKeys @@ -151,8 +153,12 @@ library test-suite tests type: exitcode-stdio-1.0 main-is: Main.hs + + -- cabal-fmt: expand test other-modules: + Main Paths_types_common + Test.Data.Mailbox Test.Data.PEMKeys Test.Domain Test.Handle @@ -214,6 +220,7 @@ test-suite tests , bytestring , bytestring-conversion , cereal + , email-validate , imports , protobuf , string-conversions @@ -224,6 +231,7 @@ test-suite tests , time , types-common , unordered-containers + , utf8-string , uuid default-language: GHC2021 diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs b/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs index 910a6c2d4b1..cc565259e44 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs @@ -35,6 +35,7 @@ data Versioned v name instance {-# OVERLAPPING #-} (RenderableSymbol a) => RenderableSymbol (Versioned v a) where renderSymbol = renderSymbol @a + renderOperationId = renderOperationId @a type family FedPath (name :: k) :: Symbol diff --git a/libs/wire-api/src/Wire/API/Call/Config.hs b/libs/wire-api/src/Wire/API/Call/Config.hs index 889b8ffd1bf..e0fafcf1f6f 100644 --- a/libs/wire-api/src/Wire/API/Call/Config.hs +++ b/libs/wire-api/src/Wire/API/Call/Config.hs @@ -466,7 +466,7 @@ mkSFTUsername shared expires rnd = } instance ToSchema SFTUsername where - schema = toText .= parsedText "" fromText + schema = toText .= parsedText "SFTUsername" fromText where fromText :: Text -> Either String SFTUsername fromText = parseOnly (parseSFTUsername <* endOfInput) @@ -543,7 +543,7 @@ turnUsername expires rnd = } instance ToSchema TurnUsername where - schema = toText .= parsedText "" fromText + schema = toText .= parsedText "TurnUsername" fromText where fromText :: Text -> Either String TurnUsername fromText = parseOnly (parseTurnUsername <* endOfInput) diff --git a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs index 17870c6a249..ef4be957f28 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs @@ -108,7 +108,7 @@ optionalActiveMLSConversationDataSchema (Just v) (description ?~ "The epoch number of the corresponding MLS group") schema <*> fmap (.epochTimestamp) - .= field "epoch_timestamp" (named "Epoch Timestamp" . nullable . unnamed $ utcTimeSchema) + .= field "epoch_timestamp" (named "EpochTimestamp" . nullable . unnamed $ utcTimeSchema) <*> maybe MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 (.ciphersuite) .= fieldWithDocModifier "cipher_suite" diff --git a/libs/wire-api/src/Wire/API/Event/Conversation.hs b/libs/wire-api/src/Wire/API/Event/Conversation.hs index f06e8d62973..74d537136a4 100644 --- a/libs/wire-api/src/Wire/API/Event/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Event/Conversation.hs @@ -409,7 +409,7 @@ taggedEventDataSchema = memberLeaveSchema :: ValueSchema NamedSwaggerDoc (EdMemberLeftReason, QualifiedUserIdList) memberLeaveSchema = - object "QualifiedUserIdList with EdMemberLeftReason" $ + object "QualifiedUserIdList_with_EdMemberLeftReason" $ (,) <$> fst .= field "reason" schema <*> snd .= qualifiedUserIdListObjectSchema instance ToSchema Event where diff --git a/libs/wire-api/src/Wire/API/Provider/Service.hs b/libs/wire-api/src/Wire/API/Provider/Service.hs index 7b181183e1e..4589dc8dc69 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service.hs @@ -282,7 +282,7 @@ data ServiceProfilePage = ServiceProfilePage instance ToSchema ServiceProfilePage where schema = - object "ServiceProfile" $ + object "ServiceProfilePage" $ ServiceProfilePage <$> serviceProfilePageHasMore .= field "has_more" schema <*> serviceProfilePageResults .= field "services" (array schema) diff --git a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs index 4b0d8e1c848..95aaaebab1d 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs @@ -180,7 +180,7 @@ instance ToByteString ServiceTag where builder WeatherTag = "weather" instance ToSchema ServiceTag where - schema = enum @Text "" . mconcat $ (\a -> element (decodeUtf8With lenientDecode $ toStrict $ toByteString a) a) <$> [minBound ..] + schema = enum @Text "ServiceTag" . mconcat $ (\a -> element (decodeUtf8With lenientDecode $ toStrict $ toByteString a) a) <$> [minBound ..] instance S.ToParamSchema ServiceTag where toParamSchema _ = diff --git a/libs/wire-api/src/Wire/API/Routes/Named.hs b/libs/wire-api/src/Wire/API/Routes/Named.hs index 91f702dd412..5978774da2a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Named.hs +++ b/libs/wire-api/src/Wire/API/Routes/Named.hs @@ -19,7 +19,7 @@ module Wire.API.Routes.Named where -import Control.Lens ((%~)) +import Control.Lens ((%~), (?~)) import Data.Kind import Data.Metrics.Servant import Data.OpenApi.Lens hiding (HasServer) @@ -42,17 +42,22 @@ newtype Named name x = Named {unnamed :: x} -- types other than string literals in some places. class RenderableSymbol a where renderSymbol :: Text + renderOperationId :: Text + renderOperationId = renderSymbol @a instance (KnownSymbol a) => RenderableSymbol a where renderSymbol = T.pack . show $ symbolVal (Proxy @a) + renderOperationId = T.pack $ symbolVal (Proxy @a) instance (RenderableSymbol a, RenderableSymbol b) => RenderableSymbol '(a, b) where renderSymbol = "(" <> (renderSymbol @a) <> ", " <> (renderSymbol @b) <> ")" + renderOperationId = renderOperationId @a <> "_" <> renderOperationId @b newtype RenderableTypeName a = RenderableTypeName a instance (GRenderableSymbol (Rep a)) => RenderableSymbol (RenderableTypeName a) where renderSymbol = grenderSymbol @(Rep a) + renderOperationId = grenderSymbol @(Rep a) class GRenderableSymbol f where grenderSymbol :: Text @@ -64,6 +69,7 @@ instance (HasOpenApi api, RenderableSymbol name) => HasOpenApi (Named name api) toOpenApi _ = toOpenApi (Proxy @api) & allOperations . description %~ (Just (dscr <> "\n\n") <>) + & allOperations . operationId ?~ renderOperationId @name where dscr :: Text dscr = diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs index 8f5719ad2d4..782e29fbb59 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs @@ -52,7 +52,7 @@ type BotAPI = :> ZAccess :> ZConn :> "conversations" - :> Capture "Conversation ID" ConvId + :> Capture "conv" ConvId :> "bots" :> ReqBody '[JSON] AddBot :> MultiVerb1 'POST '[JSON] (Respond 201 "" AddBotResponse) @@ -65,9 +65,9 @@ type BotAPI = :> ZAccess :> ZConn :> "conversations" - :> Capture "Conversation ID" ConvId + :> Capture "conv" ConvId :> "bots" - :> Capture "Bot ID" BotId + :> Capture "bot" BotId :> MultiVerb 'DELETE '[JSON] DeleteResponses (Maybe RemoveBotResponse) ) :<|> Named @@ -178,7 +178,7 @@ type BotAPI = :> ZBot :> "bot" :> "users" - :> Capture "User ID" UserId + :> Capture "user" UserId :> "clients" :> Get '[JSON] [PubClient] ) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs b/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs index 7b305f63c95..22cd1ddf5f0 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs @@ -32,6 +32,7 @@ import Wire.API.Error.Cargohold import Wire.API.Routes.API import Wire.API.Routes.AssetBody import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named import Wire.API.Routes.Public import Wire.API.Routes.QualifiedCapture import Wire.API.Routes.Version @@ -39,6 +40,15 @@ import Wire.API.Routes.Version data PrincipalTag = UserPrincipalTag | BotPrincipalTag | ProviderPrincipalTag deriving (Eq, Show) +instance RenderableSymbol UserPrincipalTag where + renderSymbol = "user" + +instance RenderableSymbol BotPrincipalTag where + renderSymbol = "bot" + +instance RenderableSymbol ProviderPrincipalTag where + renderSymbol = "provider" + type family PrincipalId (tag :: PrincipalTag) = (id :: Type) | id -> tag where PrincipalId 'UserPrincipalTag = Local UserId PrincipalId 'BotPrincipalTag = BotId @@ -126,188 +136,214 @@ type CargoholdAPI = -- This was introduced before API versioning, and the user endpoints contain a -- v3 suffix, which is removed starting from API V2. type BaseAPIv3 (tag :: PrincipalTag) = - ( Summary "Upload an asset" - :> CanThrow 'AssetTooLarge - :> CanThrow 'InvalidLength - :> tag - :> AssetBody - :> MultiVerb - 'POST - '[JSON] - '[ WithHeaders - (AssetLocationHeader Relative) - (Asset, AssetLocation Relative) - (Respond 201 "Asset posted" Asset) - ] - (Asset, AssetLocation Relative) - ) - :<|> ( Summary "Download an asset" - :> tag - :> Capture "key" AssetKey - :> Header "Asset-Token" AssetToken - :> QueryParam "asset_token" AssetToken - :> ZHostOpt - :> GetAsset - ) - :<|> ( Summary "Delete an asset" - :> CanThrow 'AssetNotFound - :> CanThrow 'Unauthorised - :> tag - :> Capture "key" AssetKey - :> MultiVerb - 'DELETE - '[JSON] - '[RespondEmpty 200 "Asset deleted"] - () - ) + Named + '("assets-upload-v3", tag) + ( Summary "Upload an asset" + :> CanThrow 'AssetTooLarge + :> CanThrow 'InvalidLength + :> tag + :> AssetBody + :> MultiVerb + 'POST + '[JSON] + '[ WithHeaders + (AssetLocationHeader Relative) + (Asset, AssetLocation Relative) + (Respond 201 "Asset posted" Asset) + ] + (Asset, AssetLocation Relative) + ) + :<|> Named + '("assets-download-v3", tag) + ( Summary "Download an asset" + :> tag + :> Capture "key" AssetKey + :> Header "Asset-Token" AssetToken + :> QueryParam "asset_token" AssetToken + :> ZHostOpt + :> GetAsset + ) + :<|> Named + '("assets-delete-v3", tag) + ( Summary "Delete an asset" + :> CanThrow 'AssetNotFound + :> CanThrow 'Unauthorised + :> tag + :> Capture "key" AssetKey + :> MultiVerb + 'DELETE + '[JSON] + '[RespondEmpty 200 "Asset deleted"] + () + ) -- | Qualified asset API. Only download and delete endpoints are supported, as -- upload has stayed unqualified. These endpoints also predate API versioning, -- and contain a v4 suffix. type QualifiedAPI = - ( Summary "Download an asset" - :> Until 'V2 - :> Description - "**Note**: local assets result in a redirect, \ - \while remote assets are streamed directly." - :> ZLocalUser - :> "assets" - :> "v4" - :> QualifiedCapture "key" AssetKey - :> Header "Asset-Token" AssetToken - :> QueryParam "asset_token" AssetToken - :> ZHostOpt - :> MultiVerb - 'GET - '() - '[ ErrorResponse 'AssetNotFound, - AssetRedirect, - AssetStreaming - ] - (Maybe LocalOrRemoteAsset) - ) - :<|> ( Summary "Delete an asset" - :> Until 'V2 - :> Description "**Note**: only local assets can be deleted." - :> CanThrow 'AssetNotFound - :> CanThrow 'Unauthorised - :> ZLocalUser - :> "assets" - :> "v4" - :> QualifiedCapture "key" AssetKey - :> MultiVerb - 'DELETE - '[JSON] - '[RespondEmpty 200 "Asset deleted"] - () - ) + Named + "assets-download-v4" + ( Summary "Download an asset" + :> Until 'V2 + :> Description + "**Note**: local assets result in a redirect, \ + \while remote assets are streamed directly." + :> ZLocalUser + :> "assets" + :> "v4" + :> QualifiedCapture "key" AssetKey + :> Header "Asset-Token" AssetToken + :> QueryParam "asset_token" AssetToken + :> ZHostOpt + :> MultiVerb + 'GET + '() + '[ ErrorResponse 'AssetNotFound, + AssetRedirect, + AssetStreaming + ] + (Maybe LocalOrRemoteAsset) + ) + :<|> Named + "assets-delete-v4" + ( Summary "Delete an asset" + :> Until 'V2 + :> Description "**Note**: only local assets can be deleted." + :> CanThrow 'AssetNotFound + :> CanThrow 'Unauthorised + :> ZLocalUser + :> "assets" + :> "v4" + :> QualifiedCapture "key" AssetKey + :> MultiVerb + 'DELETE + '[JSON] + '[RespondEmpty 200 "Asset deleted"] + () + ) -- Old endpoints, predating BaseAPIv3, and therefore API versioning. type LegacyAPI = - ( ZLocalUser - :> Until 'V2 - :> "assets" - :> QueryParam' [Required, Strict] "conv_id" ConvId - :> Capture "id" AssetId - :> GetAsset - ) - :<|> ( ZLocalUser - :> Until 'V2 - :> "conversations" - :> Capture "cnv" ConvId - :> "assets" - :> Capture "id" AssetId - :> GetAsset - ) - :<|> ( ZLocalUser - :> Until 'V2 - :> "conversations" - :> Capture "cnv" ConvId - :> "otr" - :> "assets" - :> Capture "id" AssetId - :> GetAsset - ) + Named + "assets-download-legacy" + ( ZLocalUser + :> Until 'V2 + :> "assets" + :> QueryParam' [Required, Strict] "conv_id" ConvId + :> Capture "id" AssetId + :> GetAsset + ) + :<|> Named + "assets-conv-download-legacy" + ( ZLocalUser + :> Until 'V2 + :> "conversations" + :> Capture "cnv" ConvId + :> "assets" + :> Capture "id" AssetId + :> GetAsset + ) + :<|> Named + "assets-conv-otr-download-legacy" + ( ZLocalUser + :> Until 'V2 + :> "conversations" + :> Capture "cnv" ConvId + :> "otr" + :> "assets" + :> Capture "id" AssetId + :> GetAsset + ) -- | With API versioning, the previous ad-hoc v3/v4 versioning is abandoned, and -- asset endpoints are versioned normally as part of the public API, without any -- explicit prefix. type MainAPI = - ( Summary "Renew an asset token" - :> From 'V2 - :> CanThrow 'AssetNotFound - :> CanThrow 'Unauthorised - :> ZLocalUser - :> "assets" - :> Capture "key" AssetKey - :> "token" - :> Post '[JSON] NewAssetToken - ) - :<|> ( Summary "Delete an asset token" - :> From 'V2 - :> Description "**Note**: deleting the token makes the asset public." - :> ZLocalUser - :> "assets" - :> Capture "key" AssetKey - :> "token" - :> MultiVerb - 'DELETE - '[JSON] - '[RespondEmpty 200 "Asset token deleted"] - () - ) - :<|> ( Summary "Upload an asset" - :> From 'V2 - :> CanThrow 'AssetTooLarge - :> CanThrow 'InvalidLength - :> ZLocalUser - :> "assets" - :> AssetBody - :> MultiVerb - 'POST - '[JSON] - '[ WithHeaders - (AssetLocationHeader Relative) - (Asset, AssetLocation Relative) - (Respond 201 "Asset posted" Asset) - ] - (Asset, AssetLocation Relative) - ) - :<|> ( Summary "Download an asset" - :> From 'V2 - :> Description - "**Note**: local assets result in a redirect, \ - \while remote assets are streamed directly." - :> CanThrow 'NoMatchingAssetEndpoint - :> ZLocalUser - :> "assets" - :> QualifiedCapture "key" AssetKey - :> Header "Asset-Token" AssetToken - :> QueryParam "asset_token" AssetToken - :> ZHostOpt - :> MultiVerb - 'GET - '() - '[ ErrorResponse 'AssetNotFound, - AssetRedirect, - AssetStreaming - ] - (Maybe LocalOrRemoteAsset) - ) - :<|> ( Summary "Delete an asset" - :> From 'V2 - :> Description "**Note**: only local assets can be deleted." - :> CanThrow 'AssetNotFound - :> CanThrow 'Unauthorised - :> ZLocalUser - :> "assets" - :> QualifiedCapture "key" AssetKey - :> MultiVerb - 'DELETE - '[JSON] - '[RespondEmpty 200 "Asset deleted"] - () - ) + Named + "tokens-renew" + ( Summary "Renew an asset token" + :> From 'V2 + :> CanThrow 'AssetNotFound + :> CanThrow 'Unauthorised + :> ZLocalUser + :> "assets" + :> Capture "key" AssetKey + :> "token" + :> Post '[JSON] NewAssetToken + ) + :<|> Named + "tokens-delete" + ( Summary "Delete an asset token" + :> From 'V2 + :> Description "**Note**: deleting the token makes the asset public." + :> ZLocalUser + :> "assets" + :> Capture "key" AssetKey + :> "token" + :> MultiVerb + 'DELETE + '[JSON] + '[RespondEmpty 200 "Asset token deleted"] + () + ) + :<|> Named + "assets-upload" + ( Summary "Upload an asset" + :> From 'V2 + :> CanThrow 'AssetTooLarge + :> CanThrow 'InvalidLength + :> ZLocalUser + :> "assets" + :> AssetBody + :> MultiVerb + 'POST + '[JSON] + '[ WithHeaders + (AssetLocationHeader Relative) + (Asset, AssetLocation Relative) + (Respond 201 "Asset posted" Asset) + ] + (Asset, AssetLocation Relative) + ) + :<|> Named + "assets-download" + ( Summary "Download an asset" + :> From 'V2 + :> Description + "**Note**: local assets result in a redirect, \ + \while remote assets are streamed directly." + :> CanThrow 'NoMatchingAssetEndpoint + :> ZLocalUser + :> "assets" + :> QualifiedCapture "key" AssetKey + :> Header "Asset-Token" AssetToken + :> QueryParam "asset_token" AssetToken + :> ZHostOpt + :> MultiVerb + 'GET + '() + '[ ErrorResponse 'AssetNotFound, + AssetRedirect, + AssetStreaming + ] + (Maybe LocalOrRemoteAsset) + ) + :<|> Named + "assets-delete" + ( Summary "Delete an asset" + :> From 'V2 + :> Description "**Note**: only local assets can be deleted." + :> CanThrow 'AssetNotFound + :> CanThrow 'Unauthorised + :> ZLocalUser + :> "assets" + :> QualifiedCapture "key" AssetKey + :> MultiVerb + 'DELETE + '[JSON] + '[RespondEmpty 200 "Asset deleted"] + () + ) data CargoholdAPITag diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs index 4c8282f8d71..bf87bfb3fef 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs @@ -35,6 +35,7 @@ import Wire.API.Error import Wire.API.Error.Brig import Wire.API.Routes.API import Wire.API.Routes.Internal.Spar +import Wire.API.Routes.Named import Wire.API.Routes.Public import Wire.API.SwaggerServant import Wire.API.User.IdentityProvider @@ -58,8 +59,8 @@ type DeprecateSSOAPIV1 = \Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams" type APISSO = - DeprecateSSOAPIV1 :> Deprecated :> "metadata" :> SAML.APIMeta - :<|> "metadata" :> Capture "team" TeamId :> SAML.APIMeta + Named "sso-metadata" (DeprecateSSOAPIV1 :> Deprecated :> "metadata" :> SAML.APIMeta) + :<|> Named "sso-team-metadata" ("metadata" :> Capture "team" TeamId :> SAML.APIMeta) :<|> "initiate-login" :> APIAuthReqPrecheck :<|> "initiate-login" :> APIAuthReq :<|> APIAuthRespLegacy @@ -69,40 +70,52 @@ type APISSO = type CheckOK = Verb 'HEAD 200 type APIAuthReqPrecheck = - QueryParam "success_redirect" URI.URI - :> QueryParam "error_redirect" URI.URI - :> Capture "idp" SAML.IdPId - :> CheckOK '[PlainText] NoContent + Named + "auth-req-precheck" + ( QueryParam "success_redirect" URI.URI + :> QueryParam "error_redirect" URI.URI + :> Capture "idp" SAML.IdPId + :> CheckOK '[PlainText] NoContent + ) type APIAuthReq = - QueryParam "success_redirect" URI.URI - :> QueryParam "error_redirect" URI.URI - -- (SAML.APIAuthReq from here on, except for the cookies) - :> Capture "idp" SAML.IdPId - :> Get '[SAML.HTML] (SAML.FormRedirect SAML.AuthnRequest) + Named + "auth-req" + ( QueryParam "success_redirect" URI.URI + :> QueryParam "error_redirect" URI.URI + -- (SAML.APIAuthReq from here on, except for the cookies) + :> Capture "idp" SAML.IdPId + :> Get '[SAML.HTML] (SAML.FormRedirect SAML.AuthnRequest) + ) type APIAuthRespLegacy = - DeprecateSSOAPIV1 - :> Deprecated - :> "finalize-login" - -- (SAML.APIAuthResp from here on, except for response) - :> MultipartForm Mem SAML.AuthnResponseBody - :> Post '[PlainText] Void + Named + "auth-resp-legacy" + ( DeprecateSSOAPIV1 + :> Deprecated + :> "finalize-login" + -- (SAML.APIAuthResp from here on, except for response) + :> MultipartForm Mem SAML.AuthnResponseBody + :> Post '[PlainText] Void + ) type APIAuthResp = - "finalize-login" - :> Capture "team" TeamId - -- (SAML.APIAuthResp from here on, except for response) - :> MultipartForm Mem SAML.AuthnResponseBody - :> Post '[PlainText] Void + Named + "auth-resp" + ( "finalize-login" + :> Capture "team" TeamId + -- (SAML.APIAuthResp from here on, except for response) + :> MultipartForm Mem SAML.AuthnResponseBody + :> Post '[PlainText] Void + ) type APIIDP = - ZOptUser :> IdpGet - :<|> ZOptUser :> IdpGetRaw - :<|> ZOptUser :> IdpGetAll - :<|> ZOptUser :> IdpCreate - :<|> ZOptUser :> IdpUpdate - :<|> ZOptUser :> IdpDelete + Named "idp-get" (ZOptUser :> IdpGet) + :<|> Named "idp-get-raw" (ZOptUser :> IdpGetRaw) + :<|> Named "idp-get-all" (ZOptUser :> IdpGetAll) + :<|> Named "idp-create" (ZOptUser :> IdpCreate) + :<|> Named "idp-update" (ZOptUser :> IdpUpdate) + :<|> Named "idp-delete" (ZOptUser :> IdpDelete) type IdpGetRaw = Capture "id" SAML.IdPId :> "raw" :> Get '[RawXML] RawIdPMetadata @@ -132,7 +145,10 @@ type IdpDelete = :> DeleteNoContent type SsoSettingsGet = - Get '[JSON] SsoSettings + Named + "sso-settings" + ( Get '[JSON] SsoSettings + ) sparSPIssuer :: (Functor m, SAML.HasConfig m) => Maybe TeamId -> m SAML.Issuer sparSPIssuer Nothing = @@ -172,9 +188,9 @@ data ScimSite tag route = ScimSite deriving (Generic) type APIScimToken = - ZOptUser :> APIScimTokenCreate - :<|> ZOptUser :> APIScimTokenDelete - :<|> ZOptUser :> APIScimTokenList + Named "auth-tokens-create" (ZOptUser :> APIScimTokenCreate) + :<|> Named "auth-tokens-delete" (ZOptUser :> APIScimTokenDelete) + :<|> Named "auth-tokens-list" (ZOptUser :> APIScimTokenList) type APIScimTokenCreate = ReqBody '[JSON] CreateScimToken diff --git a/libs/wire-api/src/Wire/API/Team/Invitation.hs b/libs/wire-api/src/Wire/API/Team/Invitation.hs index f195b4072ce..49fe051705a 100644 --- a/libs/wire-api/src/Wire/API/Team/Invitation.hs +++ b/libs/wire-api/src/Wire/API/Team/Invitation.hs @@ -118,7 +118,7 @@ instance ToSchema Invitation where <*> (fmap (TE.decodeUtf8 . serializeURIRef') . inviteeUrl) .= optFieldWithDocModifier "url" (description ?~ "URL of the invitation link to be sent to the invitee") (maybeWithDefault A.Null urlSchema) where - urlSchema = parsedText "URIRef Absolute" (runParser (uriParser strictURIParserOptions) . TE.encodeUtf8) + urlSchema = parsedText "URIRef_Absolute" (runParser (uriParser strictURIParserOptions) . TE.encodeUtf8) newtype InvitationLocation = InvitationLocation { unInvitationLocation :: ByteString diff --git a/libs/wire-api/src/Wire/API/User/Orphans.hs b/libs/wire-api/src/Wire/API/User/Orphans.hs index 0f019fdc1f9..316889c115a 100644 --- a/libs/wire-api/src/Wire/API/User/Orphans.hs +++ b/libs/wire-api/src/Wire/API/User/Orphans.hs @@ -103,7 +103,11 @@ instance ToSchema (SAML.FormRedirect SAML.AuthnRequest) where & properties . at "xml" ?~ authnReqSchema instance ToSchema (SAML.ID SAML.AuthnRequest) where - declareNamedSchema = genericDeclareNamedSchema samlSchemaOptions + declareNamedSchema = + genericDeclareNamedSchema + samlSchemaOptions + { datatypeNameModifier = const "Id_AuthnRequest" + } instance ToSchema SAML.Time where declareNamedSchema = genericDeclareNamedSchema samlSchemaOptions diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 2429a3a78b7..e3ee19364cc 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -414,6 +414,7 @@ let pkgs.nixpkgs-fmt pkgs.openssl pkgs.ormolu + pkgs.vacuum-go pkgs.shellcheck pkgs.treefmt pkgs.gawk diff --git a/services/brig/src/Brig/AWS.hs b/services/brig/src/Brig/AWS.hs index f6c96a7bdf8..1d86abdc0fe 100644 --- a/services/brig/src/Brig/AWS.hs +++ b/services/brig/src/Brig/AWS.hs @@ -174,10 +174,10 @@ listen throttleMillis url callback = forever . handleAny unexpectedError $ do & set SQS.receiveMessage_waitTimeSeconds (Just 20) . set SQS.receiveMessage_maxNumberOfMessages (Just 10) onMessage m = - case decodeStrict . Text.encodeUtf8 =<< (m ^. SQS.message_body) of - Nothing -> err $ msg ("Failed to parse SQS event: " ++ show m) - Just n -> do - debug $ msg ("Received SQS event: " ++ show n) + case eitherDecodeStrict . Text.encodeUtf8 =<< maybe (Left "No message body received") Right (m ^. SQS.message_body) of + Left e -> err $ msg (val "Failed to parse SQS event") . field "error" e . field "message" (show m) + Right n -> do + debug $ msg (val "Received SQS event") . field "event" (show n) liftIO $ callback n for_ (m ^. SQS.message_receiptHandle) (void . send . SQS.newDeleteMessage url) unexpectedError x = do diff --git a/services/brig/src/Brig/AWS/SesNotification.hs b/services/brig/src/Brig/AWS/SesNotification.hs index d2e803b34ed..37aca042cc5 100644 --- a/services/brig/src/Brig/AWS/SesNotification.hs +++ b/services/brig/src/Brig/AWS/SesNotification.hs @@ -22,6 +22,7 @@ where import Brig.AWS.Types import Brig.App +import Data.Mailbox import Imports import Polysemy (Member) import System.Logger.Class (field, msg, (~~)) @@ -30,26 +31,26 @@ import Wire.API.User.Identity import Wire.UserSubsystem onEvent :: (Member UserSubsystem r) => SESNotification -> AppT r () -onEvent (MailBounce BouncePermanent es) = onPermanentBounce es -onEvent (MailBounce BounceTransient es) = onTransientBounce es -onEvent (MailBounce BounceUndetermined es) = onUndeterminedBounce es -onEvent (MailComplaint es) = onComplaint es - -onPermanentBounce :: (Member UserSubsystem r) => [EmailAddress] -> AppT r () -onPermanentBounce = mapM_ $ \e -> do - logEmailEvent "Permanent bounce" e - liftSem $ blockListInsert e - -onTransientBounce :: [EmailAddress] -> AppT r () -onTransientBounce = mapM_ (logEmailEvent "Transient bounce") - -onUndeterminedBounce :: [EmailAddress] -> AppT r () -onUndeterminedBounce = mapM_ (logEmailEvent "Undetermined bounce") - -onComplaint :: (Member UserSubsystem r) => [EmailAddress] -> AppT r () -onComplaint = mapM_ $ \e -> do - logEmailEvent "Complaint" e - liftSem $ blockListInsert e +onEvent (MailBounce BouncePermanent recipients) = onPermanentBounce recipients +onEvent (MailBounce BounceTransient recipients) = onTransientBounce recipients +onEvent (MailBounce BounceUndetermined recipients) = onUndeterminedBounce recipients +onEvent (MailComplaint recipients) = onComplaint recipients + +onPermanentBounce :: (Member UserSubsystem r) => [Mailbox] -> AppT r () +onPermanentBounce = mapM_ $ \mailbox -> do + logEmailEvent "Permanent bounce" mailbox.address + liftSem $ blockListInsert mailbox.address + +onTransientBounce :: [Mailbox] -> AppT r () +onTransientBounce = mapM_ (logEmailEvent "Transient bounce" . (.address)) + +onUndeterminedBounce :: [Mailbox] -> AppT r () +onUndeterminedBounce = mapM_ (logEmailEvent "Undetermined bounce" . (.address)) + +onComplaint :: (Member UserSubsystem r) => [Mailbox] -> AppT r () +onComplaint = mapM_ $ \mailbox -> do + logEmailEvent "Complaint" mailbox.address + liftSem $ blockListInsert mailbox.address logEmailEvent :: Text -> EmailAddress -> AppT r () logEmailEvent t e = Log.info $ field "email" (fromEmail e) ~~ msg t diff --git a/services/brig/src/Brig/AWS/Types.hs b/services/brig/src/Brig/AWS/Types.hs index 75603a6ccdb..c2201d59d2f 100644 --- a/services/brig/src/Brig/AWS/Types.hs +++ b/services/brig/src/Brig/AWS/Types.hs @@ -23,15 +23,15 @@ module Brig.AWS.Types where import Data.Aeson +import Data.Mailbox import Imports -import Wire.API.User.Identity ------------------------------------------------------------------------------- -- Notifications data SESNotification - = MailBounce !SESBounceType [EmailAddress] - | MailComplaint [EmailAddress] + = MailBounce !SESBounceType [Mailbox] + | MailComplaint [Mailbox] deriving (Eq, Show) data SESBounceType diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index 9c1afb6f703..b0ba8de47d4 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -50,6 +50,7 @@ import Data.Json.Util (fromUTCTimeMillis) import Data.LegalHold import Data.List.NonEmpty qualified as NonEmpty import Data.List1 (singleton) +import Data.Mailbox import Data.Misc (plainTextPassword6Unsafe) import Data.Proxy import Data.Qualified @@ -462,8 +463,8 @@ testCreateUserBlacklist _ brig aws = publishMessage :: Text -> EmailAddress -> Text -> Http () publishMessage typ em queue = do let bdy = encode $ case typ of - "bounce" -> MailBounce BouncePermanent [em] - "complaint" -> MailComplaint [em] + "bounce" -> MailBounce BouncePermanent [Mailbox Nothing em] + "complaint" -> MailComplaint [Mailbox Nothing em] x -> error ("Unsupported message type: " ++ show x) void . AWS.execute aws $ AWS.enqueueStandard queue bdy awaitBlacklist :: Int -> EmailAddress -> Http () diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index a1baaaae223..8c39a9a7e4c 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -56,6 +56,7 @@ import Data.Handle (Handle (..)) import Data.Id import Data.List1 (List1) import Data.List1 qualified as List1 +import Data.Mailbox import Data.Misc import Data.Proxy import Data.Qualified @@ -229,6 +230,12 @@ instance ToJSON SESNotification where ] ] +instance ToJSON Mailbox where + toJSON (Mailbox mName addr) = + case mName of + Nothing -> toJSON addr + Just ns -> String $ "\"" <> T.unwords ns <> "\" <" <> T.decodeUtf8 (toByteString' addr) <> ">" + test :: Manager -> TestName -> Http a -> TestTree test m n h = testCase n (void $ runHttpT m h) diff --git a/services/cargohold/src/CargoHold/API/Public.hs b/services/cargohold/src/CargoHold/API/Public.hs index 5177e19e4a5..8b9c7cfccfd 100644 --- a/services/cargohold/src/CargoHold/API/Public.hs +++ b/services/cargohold/src/CargoHold/API/Public.hs @@ -60,21 +60,36 @@ servantSitemap = :<|> mainAPI where userAPI :: forall tag. (tag ~ 'UserPrincipalTag) => ServerT (BaseAPIv3 tag) Handler - userAPI = uploadAssetV3 @tag :<|> downloadAssetV3 @tag :<|> deleteAssetV3 @tag + userAPI = + Named @'("assets-upload-v3", tag) uploadAssetV3 + :<|> Named @'("assets-download-v3", tag) downloadAssetV3 + :<|> Named @'("assets-delete-v3", tag) deleteAssetV3 botAPI :: forall tag. (tag ~ 'BotPrincipalTag) => ServerT (BaseAPIv3 tag) Handler - botAPI = uploadAssetV3 @tag :<|> downloadAssetV3 @tag :<|> deleteAssetV3 @tag + botAPI = + Named @'("assets-upload-v3", tag) uploadAssetV3 + :<|> Named @'("assets-download-v3", tag) downloadAssetV3 + :<|> Named @'("assets-delete-v3", tag) deleteAssetV3 providerAPI :: forall tag. (tag ~ 'ProviderPrincipalTag) => ServerT (BaseAPIv3 tag) Handler - providerAPI = uploadAssetV3 @tag :<|> downloadAssetV3 @tag :<|> deleteAssetV3 @tag - legacyAPI = legacyDownloadPlain :<|> legacyDownloadPlain :<|> legacyDownloadOtr + providerAPI = + Named @'("assets-upload-v3", tag) uploadAssetV3 + :<|> Named @'("assets-download-v3", tag) downloadAssetV3 + :<|> Named @'("assets-delete-v3", tag) deleteAssetV3 + legacyAPI = + Named @"assets-download-legacy" legacyDownloadPlain + :<|> Named @"assets-conv-download-legacy" legacyDownloadPlain + :<|> Named @"assets-conv-otr-download-legacy" legacyDownloadOtr qualifiedAPI :: ServerT QualifiedAPI Handler - qualifiedAPI = downloadAssetV4 :<|> deleteAssetV4 + qualifiedAPI = + Named @"assets-download-v4" + downloadAssetV4 + :<|> Named @"assets-delete-v4" deleteAssetV4 mainAPI :: ServerT MainAPI Handler mainAPI = - renewTokenV3 - :<|> deleteTokenV3 - :<|> uploadAssetV3 @'UserPrincipalTag - :<|> downloadAssetV4 - :<|> deleteAssetV4 + Named @"tokens-renew" renewTokenV3 + :<|> Named @"tokens-delete" deleteTokenV3 + :<|> Named @"assets-upload" (uploadAssetV3 @'UserPrincipalTag) + :<|> Named @"assets-download" downloadAssetV4 + :<|> Named @"assets-delete" deleteAssetV4 internalSitemap :: ServerT InternalAPI Handler internalSitemap = diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index f814f211402..49399d77be3 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -103,6 +103,7 @@ import qualified Spar.Sem.VerdictFormatStore as VerdictFormatStore import System.Logger (Msg) import qualified URI.ByteString as URI import Wire.API.Routes.Internal.Spar +import Wire.API.Routes.Named import Wire.API.Routes.Public.Spar import Wire.API.Team.Member (HiddenPerm (CreateUpdateDeleteIdp, ReadIdp)) import Wire.API.User @@ -183,13 +184,13 @@ apiSSO :: Opts -> ServerT APISSO (Sem r) apiSSO opts = - SAML2.meta appName (SamlProtocolSettings.spIssuer Nothing) (SamlProtocolSettings.responseURI Nothing) - :<|> (\tid -> SAML2.meta appName (SamlProtocolSettings.spIssuer (Just tid)) (SamlProtocolSettings.responseURI (Just tid))) - :<|> authreqPrecheck - :<|> authreq (maxttlAuthreqDiffTime opts) - :<|> authresp Nothing - :<|> authresp . Just - :<|> ssoSettings + Named @"sso-metadata" (SAML2.meta appName (SamlProtocolSettings.spIssuer Nothing) (SamlProtocolSettings.responseURI Nothing)) + :<|> Named @"sso-team-metadata" (\tid -> SAML2.meta appName (SamlProtocolSettings.spIssuer (Just tid)) (SamlProtocolSettings.responseURI (Just tid))) + :<|> Named @"auth-req-precheck" authreqPrecheck + :<|> Named @"auth-req" (authreq (maxttlAuthreqDiffTime opts)) + :<|> Named @"auth-resp-legacy" (authresp Nothing) + :<|> Named @"auth-resp" (authresp . Just) + :<|> Named @"sso-settings" ssoSettings apiIDP :: ( Member Random r, @@ -204,12 +205,12 @@ apiIDP :: ) => ServerT APIIDP (Sem r) apiIDP = - idpGet -- get, json, captures idp id - :<|> idpGetRaw -- get, raw xml, capture idp id - :<|> idpGetAll -- get, json - :<|> idpCreate -- post, created - :<|> idpUpdate -- put, okay - :<|> idpDelete -- delete, no content + Named @"idp-get" idpGet -- get, json, captures idp id + :<|> Named @"idp-get-raw" idpGetRaw -- get, raw xml, capture idp id + :<|> Named @"idp-get-all" idpGetAll -- get, json + :<|> Named @"idp-create" idpCreate -- post, created + :<|> Named @"idp-update" idpUpdate -- put, okay + :<|> Named @"idp-delete" idpDelete -- delete, no content apiINTERNAL :: ( Member ScimTokenStore r, diff --git a/services/spar/src/Spar/Scim/Auth.hs b/services/spar/src/Spar/Scim/Auth.hs index 35e2b6a394f..45d34e667af 100644 --- a/services/spar/src/Spar/Scim/Auth.hs +++ b/services/spar/src/Spar/Scim/Auth.hs @@ -60,6 +60,7 @@ import qualified Spar.Sem.ScimTokenStore as ScimTokenStore import qualified Web.Scim.Class.Auth as Scim.Class.Auth import qualified Web.Scim.Handler as Scim import qualified Web.Scim.Schema.Error as Scim +import Wire.API.Routes.Named import Wire.API.Routes.Public.Spar (APIScimToken) import Wire.API.User as User import Wire.API.User.Scim as Api @@ -97,9 +98,9 @@ apiScimToken :: ) => ServerT APIScimToken (Sem r) apiScimToken = - createScimToken - :<|> deleteScimToken - :<|> listScimTokens + Named @"auth-tokens-create" createScimToken + :<|> Named @"auth-tokens-delete" deleteScimToken + :<|> Named @"auth-tokens-list" listScimTokens -- | > docs/reference/provisioning/scim-token.md {#RefScimTokenCreate} --