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} --