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/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/integration/test/Test/Swagger.hs b/integration/test/Test/Swagger.hs
index b7f7618092c..e68b4d6cfdd 100644
--- a/integration/test/Test/Swagger.hs
+++ b/integration/test/Test/Swagger.hs
@@ -4,8 +4,12 @@ import qualified API.Brig as BrigP
import qualified Data.Set as Set
import Data.String.Conversions
import GHC.Stack
+import System.Exit
+import System.Process
import Testlib.Assertions
import Testlib.Prelude
+import UnliftIO.Directory
+import UnliftIO.Temporary
existingVersions :: Set Int
existingVersions = Set.fromList [0, 1, 2, 3, 4, 5, 6, 7]
@@ -80,3 +84,17 @@ testSwaggerToc = do
html :: String
html = "
please pick an api version
/v0/api/swagger-ui/
/v1/api/swagger-ui/
/v2/api/swagger-ui/
/v3/api/swagger-ui/
/v4/api/swagger-ui/
/v5/api/swagger-ui/
/v6/api/swagger-ui/
/v7/api/swagger-ui/
"
+
+-- | This runs the swagger linter [vacuum](https://quobix.com/vacuum/). There is also a make
+-- rule that does this, for convenience in your develop flow. In order to run vacuum on CI, we need a running brig (or at least but running this this test case
+-- makes it easier to do this in the integration test job on our CI. vacuum
+testSwaggerLint :: (HasCallStack) => App ()
+testSwaggerLint = do
+ withSystemTempDirectory "testSwaggerLint-XXX.swagger.json" $ \swaggerFile ->
+ withCurrentDirectory swaggerFile $ do
+ let url = "http://localhost:8082/v" <> show (maximum existingVersions) <> "/api/swagger.json"
+ cmd = shell ("curl " <> url <> " > swagger.json && vacuum lint swagger.json 2>&1")
+ outcome@(exitCode, _out, _err) <- liftIO $ readCreateProcessWithExitCode cmd ""
+ case exitCode of
+ ExitSuccess -> pure ()
+ _ -> error (show outcome)
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/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.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs
index 424ac403d81..c27342c324d 100644
--- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs
+++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs
@@ -168,12 +168,26 @@ type UserAPI =
:> QualifiedCaptureUserId "uid"
:> GetUserVerb
)
+ :<|> Named
+ "update-user-email@v6"
+ ( Summary "Resend email address validation email."
+ :> Until 'V7
+ :> Description "If the user has a pending email validation, the validation email will be resent."
+ :> ZUser
+ :> "users"
+ :> CaptureUserId "uid"
+ :> "email"
+ :> ReqBody '[JSON] EmailUpdate
+ :> Put '[JSON] ()
+ )
:<|> Named
"update-user-email"
( Summary "Resend email address validation email."
+ :> From 'V7
:> Description "If the user has a pending email validation, the validation email will be resent."
:> ZUser
:> "users"
+ :> "uid"
:> CaptureUserId "uid"
:> "email"
:> ReqBody '[JSON] EmailUpdate
@@ -254,12 +268,29 @@ type UserAPI =
:> ReqBody '[JSON] SendVerificationCode
:> MultiVerb 'POST '[JSON] '[RespondEmpty 200 "Verification code sent."] ()
)
+ :<|> Named
+ "get-rich-info@v6"
+ ( Summary "Get a user's rich info"
+ :> Until 'V7
+ :> CanThrow 'InsufficientTeamPermissions
+ :> ZLocalUser
+ :> "users"
+ :> CaptureUserId "uid"
+ :> "rich-info"
+ :> MultiVerb
+ 'GET
+ '[JSON]
+ '[Respond 200 "Rich info about the user" RichInfoAssocList]
+ RichInfoAssocList
+ )
:<|> Named
"get-rich-info"
( Summary "Get a user's rich info"
+ :> From 'V7
:> CanThrow 'InsufficientTeamPermissions
:> ZLocalUser
:> "users"
+ :> "uid"
:> CaptureUserId "uid"
:> "rich-info"
:> MultiVerb
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..4dfc1bf6b2d 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,218 @@ 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."
+ :> "get-asset"
+ :> "stream-asset"
+ :> 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."
+ :> "get-asset"
+ :> "stream-asset"
+ :> 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/API/Public.hs b/services/brig/src/Brig/API/Public.hs
index 2485961ce68..9e0a6b73e73 100644
--- a/services/brig/src/Brig/API/Public.hs
+++ b/services/brig/src/Brig/API/Public.hs
@@ -333,6 +333,7 @@ servantSitemap =
userAPI =
Named @"get-user-unqualified" getUserUnqualifiedH
:<|> Named @"get-user-qualified" getUserProfileH
+ :<|> Named @"update-user-email@v6" updateUserEmail
:<|> Named @"update-user-email" updateUserEmail
:<|> Named @"get-handle-info-unqualified" getHandleInfoUnqualifiedH
:<|> Named @"get-user-by-handle-qualified" Handle.getHandleInfo
@@ -340,6 +341,7 @@ servantSitemap =
:<|> Named @"list-users-by-ids-or-handles" listUsersByIdsOrHandles
:<|> Named @"list-users-by-ids-or-handles@V3" listUsersByIdsOrHandlesV3
:<|> Named @"send-verification-code" sendVerificationCode
+ :<|> Named @"get-rich-info@v6" getRichInfo
:<|> Named @"get-rich-info" getRichInfo
:<|> Named @"get-supported-protocols" getSupportedProtocols
diff --git a/services/cargohold/src/CargoHold/API/Public.hs b/services/cargohold/src/CargoHold/API/Public.hs
index 5177e19e4a5..e80f157a9ce 100644
--- a/services/cargohold/src/CargoHold/API/Public.hs
+++ b/services/cargohold/src/CargoHold/API/Public.hs
@@ -60,21 +60,35 @@ 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}
--