diff --git a/changelog.d/4-docs/WPB-5098 b/changelog.d/4-docs/WPB-5098 new file mode 100644 index 00000000000..9b7f171dec8 --- /dev/null +++ b/changelog.d/4-docs/WPB-5098 @@ -0,0 +1 @@ +Backend-to-backend OpenApi Docs added diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index 83f89007609..dfec77bed8b 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -162,6 +162,10 @@ nginx_conf: disable_zauth: true envs: - staging + - path: /api-federation/swagger-ui + disable_zauth: true + envs: + - staging - path: /self$ # Matches exactly /self oauth_scope: self envs: diff --git a/docs/src/understand/api-client-perspective/swagger.md b/docs/src/understand/api-client-perspective/swagger.md index a60890951d9..b466fc7f8b8 100644 --- a/docs/src/understand/api-client-perspective/swagger.md +++ b/docs/src/understand/api-client-perspective/swagger.md @@ -84,6 +84,13 @@ The URL pattern is similar to that of public endpoints for latest version: If you want to get the raw json of the swagger: `https:///api-internal/swagger-ui/-swagger.json`. +### Federation API + +- Unversioned + - [`brig` - Federation API](https://staging-nginz-https.zinfra.io/api-federation/swagger-ui/brig) + - [`galley` - Federation API](https://staging-nginz-https.zinfra.io/api-federation/swagger-ui/galley) + - [`cargohold` - Federation API](https://staging-nginz-https.zinfra.io/api-federation/swagger-ui/cargohold) + ### Finding the source code for an end-point A *route internal ID* is provided for every end-point. See diff --git a/libs/types-common/src/Data/Json/Util.hs b/libs/types-common/src/Data/Json/Util.hs index 408dfe41cbc..2c898487f04 100644 --- a/libs/types-common/src/Data/Json/Util.hs +++ b/libs/types-common/src/Data/Json/Util.hs @@ -196,7 +196,7 @@ toJSONFieldName = A.defaultOptions {A.fieldLabelModifier = A.camelTo2 '_'} -- Some related discussion: . newtype Base64ByteString = Base64ByteString {fromBase64ByteString :: ByteString} deriving stock (Eq, Ord, Show) - deriving (FromJSON, ToJSON) via Schema Base64ByteString + deriving (FromJSON, ToJSON, S.ToSchema) via Schema Base64ByteString deriving newtype (Arbitrary, IsString) instance ToSchema Base64ByteString where diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/JSONResponse.hs b/libs/wai-utilities/src/Network/Wai/Utilities/JSONResponse.hs index b3afe3f5432..c2ab383a42e 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/JSONResponse.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/JSONResponse.hs @@ -24,6 +24,7 @@ where import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A +import Data.OpenApi qualified as S import Data.Schema import Imports import Network.HTTP.Types.Status @@ -43,7 +44,7 @@ data JSONResponse = JSONResponse value :: A.Value } deriving (Eq, Ord, Show) - deriving (FromJSON, ToJSON) via Schema JSONResponse + deriving (FromJSON, ToJSON, S.ToSchema) via Schema JSONResponse instance ToSchema JSONResponse where schema = diff --git a/libs/wire-api-federation/default.nix b/libs/wire-api-federation/default.nix index 78342495155..c4af614faee 100644 --- a/libs/wire-api-federation/default.nix +++ b/libs/wire-api-federation/default.nix @@ -33,6 +33,7 @@ , servant , servant-client , servant-client-core +, servant-openapi3 , servant-server , singletons , singletons-th @@ -74,6 +75,7 @@ mkDerivation { servant servant-client servant-client-core + servant-openapi3 servant-server singletons-th text diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs index 8703e3d8501..ed3586aa373 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs @@ -25,8 +25,11 @@ import Data.Aeson import Data.Domain (Domain) import Data.Handle (Handle) import Data.Id +import Data.OpenApi (OpenApi, ToSchema) +import Data.Proxy (Proxy (Proxy)) import Imports import Servant.API +import Servant.OpenApi (HasOpenApi (toOpenApi)) import Test.QuickCheck (Arbitrary) import Wire.API.Federation.API.Brig.Notifications as Notifications import Wire.API.Federation.Endpoint @@ -49,6 +52,8 @@ instance ToJSON SearchRequest instance FromJSON SearchRequest +instance ToSchema SearchRequest + data SearchResponse = SearchResponse { contacts :: [Contact], searchPolicy :: FederatedUserSearchPolicy @@ -59,6 +64,8 @@ instance ToJSON SearchResponse instance FromJSON SearchResponse +instance ToSchema SearchResponse + -- | For conventions see /docs/developer/federation-api-conventions.md type BrigApi = FedEndpoint "api-version" () VersionInfo @@ -85,6 +92,8 @@ newtype DomainSet = DomainSet deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded DomainSet) +instance ToSchema DomainSet + newtype NonConnectedBackends = NonConnectedBackends -- TODO: -- The encoding rules that were in place would make this "connectedBackends" over the wire. @@ -94,12 +103,16 @@ newtype NonConnectedBackends = NonConnectedBackends deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded NonConnectedBackends) +instance ToSchema NonConnectedBackends + newtype GetUserClients = GetUserClients { users :: [UserId] } deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded GetUserClients) +instance ToSchema GetUserClients + data MLSClientsRequest = MLSClientsRequest { userId :: UserId, -- implicitly qualified by the local domain cipherSuite :: CipherSuite @@ -107,6 +120,8 @@ data MLSClientsRequest = MLSClientsRequest deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded MLSClientsRequest) +instance ToSchema MLSClientsRequest + -- NOTE: ConversationId for remote connections -- -- The plan is to model the connect/one2one conversationId as deterministically derived from @@ -134,6 +149,8 @@ data NewConnectionRequest = NewConnectionRequest deriving (Arbitrary) via (GenericUniform NewConnectionRequest) deriving (FromJSON, ToJSON) via (CustomEncoded NewConnectionRequest) +instance ToSchema NewConnectionRequest + data RemoteConnectionAction = RemoteConnect | RemoteRescind @@ -141,6 +158,8 @@ data RemoteConnectionAction deriving (Arbitrary) via (GenericUniform RemoteConnectionAction) deriving (FromJSON, ToJSON) via (CustomEncoded RemoteConnectionAction) +instance ToSchema RemoteConnectionAction + data NewConnectionResponse = NewConnectionResponseUserNotActivated | NewConnectionResponseOk (Maybe RemoteConnectionAction) @@ -148,6 +167,8 @@ data NewConnectionResponse deriving (Arbitrary) via (GenericUniform NewConnectionResponse) deriving (FromJSON, ToJSON) via (CustomEncoded NewConnectionResponse) +instance ToSchema NewConnectionResponse + data ClaimKeyPackageRequest = ClaimKeyPackageRequest { -- | The user making the request, implictly qualified by the origin domain. claimant :: UserId, @@ -160,3 +181,8 @@ data ClaimKeyPackageRequest = ClaimKeyPackageRequest deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ClaimKeyPackageRequest) deriving (FromJSON, ToJSON) via (CustomEncoded ClaimKeyPackageRequest) + +instance ToSchema ClaimKeyPackageRequest + +swaggerDoc :: OpenApi +swaggerDoc = toOpenApi (Proxy @BrigApi) diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Brig/Notifications.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Brig/Notifications.hs index efdc16722b9..884b0c485ed 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Brig/Notifications.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Brig/Notifications.hs @@ -19,6 +19,7 @@ module Wire.API.Federation.API.Brig.Notifications where import Data.Aeson import Data.Id +import Data.OpenApi (ToSchema) import Data.Range import Imports import Wire.API.Federation.Component @@ -50,6 +51,8 @@ instance HasNotificationEndpoint 'OnUserDeletedConnectionsTag where NotificationAPI 'OnUserDeletedConnectionsTag 'Brig = NotificationFedEndpoint 'OnUserDeletedConnectionsTag +instance ToSchema UserDeletedConnectionsNotification + -- | All the notification endpoints return an 'EmptyResponse'. type BrigNotificationAPI = -- FUTUREWORK: Use NotificationAPI 'OnUserDeletedConnectionsTag 'Brig instead diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Cargohold.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Cargohold.hs index debe7a2a5d5..cb81f6cbc0d 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Cargohold.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Cargohold.hs @@ -19,8 +19,11 @@ module Wire.API.Federation.API.Cargohold where import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Id +import Data.OpenApi +import Data.Proxy import Imports import Servant.API +import Servant.OpenApi (HasOpenApi (toOpenApi)) import Wire.API.Asset import Wire.API.Federation.Endpoint import Wire.API.Routes.AssetBody @@ -28,9 +31,9 @@ import Wire.API.Util.Aeson import Wire.Arbitrary (Arbitrary, GenericUniform (..)) data GetAsset = GetAsset - { -- | User requesting the asset. Implictly qualified with the source domain. + { -- | User requesting the asset. Implicitly qualified with the source domain. user :: UserId, - -- | Asset key for the asset to download. Implictly qualified with the + -- | Asset key for the asset to download. Implicitly qualified with the -- target domain. key :: AssetKey, -- | Optional asset token. @@ -40,12 +43,19 @@ data GetAsset = GetAsset deriving (Arbitrary) via (GenericUniform GetAsset) deriving (ToJSON, FromJSON) via (CustomEncoded GetAsset) +instance ToSchema GetAsset + data GetAssetResponse = GetAssetResponse {available :: Bool} deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform GetAssetResponse) deriving (ToJSON, FromJSON) via (CustomEncoded GetAssetResponse) +instance ToSchema GetAssetResponse + type CargoholdApi = FedEndpoint "get-asset" GetAsset GetAssetResponse :<|> StreamingFedEndpoint "stream-asset" GetAsset AssetSource + +swaggerDoc :: OpenApi +swaggerDoc = toOpenApi (Proxy @CargoholdApi) diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Common.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Common.hs index 81fa5e70f55..5982de6ee77 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Common.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Common.hs @@ -18,6 +18,7 @@ module Wire.API.Federation.API.Common where import Data.Aeson +import Data.OpenApi (ToSchema) import Imports import Test.QuickCheck import Wire.Arbitrary @@ -29,6 +30,8 @@ data EmptyResponse = EmptyResponse deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform EmptyResponse) +instance ToSchema EmptyResponse + instance FromJSON EmptyResponse where parseJSON = withObject "EmptyResponse" . const $ pure EmptyResponse diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs index f40417e303b..c548cd35c60 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs @@ -26,11 +26,14 @@ import Data.Domain import Data.Id import Data.Json.Util import Data.Misc (Milliseconds) +import Data.OpenApi (OpenApi, ToSchema) +import Data.Proxy (Proxy (Proxy)) import Data.Qualified import Data.Time.Clock (UTCTime) import Imports import Network.Wai.Utilities.JSONResponse import Servant.API +import Servant.OpenApi (HasOpenApi (toOpenApi)) import Wire.API.Conversation import Wire.API.Conversation.Action import Wire.API.Conversation.Protocol @@ -147,12 +150,16 @@ data TypingDataUpdateRequest = TypingDataUpdateRequest deriving stock (Eq, Show, Generic) deriving (FromJSON, ToJSON) via (CustomEncoded TypingDataUpdateRequest) +instance ToSchema TypingDataUpdateRequest + data TypingDataUpdateResponse = TypingDataUpdateSuccess TypingDataUpdated | TypingDataUpdateError GalleyError deriving stock (Eq, Show, Generic) deriving (FromJSON, ToJSON) via (CustomEncoded TypingDataUpdateResponse) +instance ToSchema TypingDataUpdateResponse + data TypingDataUpdated = TypingDataUpdated { time :: UTCTime, origUserId :: Qualified UserId, @@ -165,6 +172,8 @@ data TypingDataUpdated = TypingDataUpdated deriving stock (Eq, Show, Generic) deriving (FromJSON, ToJSON) via (CustomEncoded TypingDataUpdated) +instance ToSchema TypingDataUpdated + data GetConversationsRequest = GetConversationsRequest { userId :: UserId, convIds :: [ConvId] @@ -173,6 +182,8 @@ data GetConversationsRequest = GetConversationsRequest deriving (Arbitrary) via (GenericUniform GetConversationsRequest) deriving (ToJSON, FromJSON) via (CustomEncoded GetConversationsRequest) +instance ToSchema GetConversationsRequest + data GetOne2OneConversationRequest = GetOne2OneConversationRequest { -- The user on the sender's domain goocSenderUser :: UserId, @@ -183,6 +194,8 @@ data GetOne2OneConversationRequest = GetOne2OneConversationRequest deriving (Arbitrary) via (GenericUniform GetOne2OneConversationRequest) deriving (ToJSON, FromJSON) via (CustomEncoded GetOne2OneConversationRequest) +instance ToSchema GetOne2OneConversationRequest + data RemoteConvMembers = RemoteConvMembers { selfRole :: RoleName, others :: [OtherMember] @@ -191,6 +204,8 @@ data RemoteConvMembers = RemoteConvMembers deriving (Arbitrary) via (GenericUniform RemoteConvMembers) deriving (FromJSON, ToJSON) via (CustomEncoded RemoteConvMembers) +instance ToSchema RemoteConvMembers + -- | A conversation hosted on a remote backend. This contains the same -- information as a 'Conversation', with the exception that conversation status -- fields (muted\/archived\/hidden) are omitted, since they are not known by the @@ -207,6 +222,8 @@ data RemoteConversation = RemoteConversation deriving (Arbitrary) via (GenericUniform RemoteConversation) deriving (FromJSON, ToJSON) via (CustomEncoded RemoteConversation) +instance ToSchema RemoteConversation + newtype GetConversationsResponse = GetConversationsResponse { convs :: [RemoteConversation] } @@ -214,6 +231,8 @@ newtype GetConversationsResponse = GetConversationsResponse deriving (Arbitrary) via (GenericUniform GetConversationsResponse) deriving (ToJSON, FromJSON) via (CustomEncoded GetConversationsResponse) +instance ToSchema GetConversationsResponse + data GetOne2OneConversationResponse = GetOne2OneConversationOk RemoteConversation | -- | This is returned when the local backend is asked for a 1-1 conversation @@ -226,6 +245,8 @@ data GetOne2OneConversationResponse deriving (Arbitrary) via (GenericUniform GetOne2OneConversationResponse) deriving (ToJSON, FromJSON) via (CustomEncoded GetOne2OneConversationResponse) +instance ToSchema GetOne2OneConversationResponse + -- | A record type describing a new federated conversation -- -- FUTUREWORK: Think about extracting common conversation metadata into a @@ -254,6 +275,8 @@ data ConversationCreated conv = ConversationCreated deriving stock (Eq, Show, Generic, Functor) deriving (ToJSON, FromJSON) via (CustomEncoded (ConversationCreated conv)) +instance (ToSchema a) => ToSchema (ConversationCreated a) + ccRemoteOrigUserId :: ConversationCreated (Remote ConvId) -> Remote UserId ccRemoteOrigUserId cc = qualifyAs cc.cnvId cc.origUserId @@ -268,15 +291,17 @@ data LeaveConversationRequest = LeaveConversationRequest deriving stock (Generic, Eq, Show) deriving (ToJSON, FromJSON) via (CustomEncoded LeaveConversationRequest) +instance ToSchema LeaveConversationRequest + -- | Error outcomes of the leave-conversation RPC. data RemoveFromConversationError = RemoveFromConversationErrorRemovalNotAllowed | RemoveFromConversationErrorNotFound | RemoveFromConversationErrorUnchanged deriving stock (Eq, Show, Generic) - deriving - (ToJSON, FromJSON) - via (CustomEncoded RemoveFromConversationError) + deriving (ToJSON, FromJSON) via (CustomEncoded RemoveFromConversationError) + +instance ToSchema RemoveFromConversationError data RemoteMLSMessageResponse = RemoteMLSMessageOk @@ -284,6 +309,8 @@ data RemoteMLSMessageResponse deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded RemoteMLSMessageResponse) +instance ToSchema RemoteMLSMessageResponse + data ProteusMessageSendRequest = ProteusMessageSendRequest { -- | Conversation is assumed to be owned by the target domain, this allows -- us to protect against relay attacks @@ -297,6 +324,8 @@ data ProteusMessageSendRequest = ProteusMessageSendRequest deriving (Arbitrary) via (GenericUniform ProteusMessageSendRequest) deriving (ToJSON, FromJSON) via (CustomEncoded ProteusMessageSendRequest) +instance ToSchema ProteusMessageSendRequest + data MLSMessageSendRequest = MLSMessageSendRequest { -- | Conversation (or sub conversation) is assumed to be owned by the target -- domain, this allows us to protect against relay attacks @@ -311,9 +340,11 @@ data MLSMessageSendRequest = MLSMessageSendRequest deriving (Arbitrary) via (GenericUniform MLSMessageSendRequest) deriving (ToJSON, FromJSON) via (CustomEncoded MLSMessageSendRequest) +instance ToSchema MLSMessageSendRequest + newtype MessageSendResponse = MessageSendResponse {response :: PostOtrResponse MessageSendingStatus} - deriving stock (Eq, Show) + deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via ( Either @@ -321,13 +352,17 @@ newtype MessageSendResponse = MessageSendResponse MessageSendingStatus ) +instance ToSchema MessageSendResponse + newtype LeaveConversationResponse = LeaveConversationResponse {response :: Either RemoveFromConversationError ()} - deriving stock (Eq, Show) + deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (Either (CustomEncoded RemoveFromConversationError) ()) +instance ToSchema LeaveConversationResponse + data ConversationUpdateRequest = ConversationUpdateRequest { -- | The user that is attempting to perform the action. This is qualified -- implicitly by the origin domain @@ -341,6 +376,8 @@ data ConversationUpdateRequest = ConversationUpdateRequest deriving (Arbitrary) via (GenericUniform ConversationUpdateRequest) deriving (FromJSON, ToJSON) via (CustomEncoded ConversationUpdateRequest) +instance ToSchema ConversationUpdateRequest + data ConversationUpdateResponse = ConversationUpdateResponseError GalleyError | ConversationUpdateResponseUpdate ConversationUpdate @@ -352,6 +389,8 @@ data ConversationUpdateResponse (ToJSON, FromJSON) via (CustomEncoded ConversationUpdateResponse) +instance ToSchema ConversationUpdateResponse + -- | A wrapper around a raw welcome message data MLSWelcomeRequest = MLSWelcomeRequest { -- | Implicitely qualified by origin domain @@ -367,12 +406,16 @@ data MLSWelcomeRequest = MLSWelcomeRequest deriving (Arbitrary) via (GenericUniform MLSWelcomeRequest) deriving (FromJSON, ToJSON) via (CustomEncoded MLSWelcomeRequest) +instance ToSchema MLSWelcomeRequest + data MLSWelcomeResponse = MLSWelcomeSent | MLSWelcomeMLSNotEnabled deriving stock (Eq, Generic, Show) deriving (FromJSON, ToJSON) via (CustomEncoded MLSWelcomeResponse) +instance ToSchema MLSWelcomeResponse + data MLSMessageResponse = MLSMessageResponseError GalleyError | MLSMessageResponseProtocolError Text @@ -387,6 +430,8 @@ data MLSMessageResponse deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded MLSMessageResponse) +instance ToSchema MLSMessageResponse + data GetGroupInfoRequest = GetGroupInfoRequest { -- | Conversation (or subconversation) is assumed to be owned by the target -- domain, this allows us to protect against relay attacks @@ -399,12 +444,16 @@ data GetGroupInfoRequest = GetGroupInfoRequest deriving (Arbitrary) via (GenericUniform GetGroupInfoRequest) deriving (ToJSON, FromJSON) via (CustomEncoded GetGroupInfoRequest) +instance ToSchema GetGroupInfoRequest + data GetGroupInfoResponse = GetGroupInfoResponseError GalleyError | GetGroupInfoResponseState Base64ByteString deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded GetGroupInfoResponse) +instance ToSchema GetGroupInfoResponse + data GetSubConversationsRequest = GetSubConversationsRequest { gsreqUser :: UserId, gsreqConv :: ConvId, @@ -413,12 +462,16 @@ data GetSubConversationsRequest = GetSubConversationsRequest deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded GetSubConversationsRequest) +instance ToSchema GetSubConversationsRequest + data GetSubConversationsResponse = GetSubConversationsResponseError GalleyError | GetSubConversationsResponseSuccess PublicSubConversation deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded GetSubConversationsResponse) +instance ToSchema GetSubConversationsResponse + data LeaveSubConversationRequest = LeaveSubConversationRequest { lscrUser :: UserId, lscrClient :: ClientId, @@ -429,6 +482,8 @@ data LeaveSubConversationRequest = LeaveSubConversationRequest deriving (Arbitrary) via (GenericUniform LeaveSubConversationRequest) deriving (ToJSON, FromJSON) via (CustomEncoded LeaveSubConversationRequest) +instance ToSchema LeaveSubConversationRequest + data LeaveSubConversationResponse = LeaveSubConversationResponseError GalleyError | LeaveSubConversationResponseProtocolError Text @@ -436,6 +491,8 @@ data LeaveSubConversationResponse deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded LeaveSubConversationResponse) +instance ToSchema LeaveSubConversationResponse + data DeleteSubConversationFedRequest = DeleteSubConversationFedRequest { dscreqUser :: UserId, dscreqConv :: ConvId, @@ -446,8 +503,15 @@ data DeleteSubConversationFedRequest = DeleteSubConversationFedRequest deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded DeleteSubConversationFedRequest) +instance ToSchema DeleteSubConversationFedRequest + data DeleteSubConversationResponse = DeleteSubConversationResponseError GalleyError | DeleteSubConversationResponseSuccess deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded DeleteSubConversationResponse) + +instance ToSchema DeleteSubConversationResponse + +swaggerDoc :: OpenApi +swaggerDoc = toOpenApi (Proxy @GalleyApi) diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs index e5a401f3940..3485a60c1c7 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs @@ -24,6 +24,7 @@ import Data.Aeson import Data.Id import Data.Json.Util import Data.List.NonEmpty +import Data.OpenApi (ToSchema) import Data.Qualified import Data.Range import Data.Time.Clock @@ -114,6 +115,8 @@ data ClientRemovedRequest = ClientRemovedRequest deriving (Arbitrary) via (GenericUniform ClientRemovedRequest) deriving (FromJSON, ToJSON) via (CustomEncoded ClientRemovedRequest) +instance ToSchema ClientRemovedRequest + -- Note: this is parametric in the conversation type to allow it to be used -- both for conversations with a fixed known domain (e.g. as the argument of the -- federation RPC), and for conversations with an arbitrary Qualified or Remote id @@ -133,6 +136,8 @@ data RemoteMessage conv = RemoteMessage deriving (Arbitrary) via (GenericUniform (RemoteMessage conv)) deriving (ToJSON, FromJSON) via (CustomEncodedLensable (RemoteMessage conv)) +instance (ToSchema a) => ToSchema (RemoteMessage a) + data RemoteMLSMessage = RemoteMLSMessage { time :: UTCTime, metadata :: MessageMetadata, @@ -146,6 +151,8 @@ data RemoteMLSMessage = RemoteMLSMessage deriving (Arbitrary) via (GenericUniform RemoteMLSMessage) deriving (ToJSON, FromJSON) via (CustomEncoded RemoteMLSMessage) +instance ToSchema RemoteMLSMessage + data ConversationUpdate = ConversationUpdate { cuTime :: UTCTime, cuOrigUserId :: Qualified UserId, @@ -168,6 +175,8 @@ instance ToJSON ConversationUpdate instance FromJSON ConversationUpdate +instance ToSchema ConversationUpdate + type UserDeletedNotificationMaxConvs = 1000 data UserDeletedConversationsNotification = UserDeletedConversationsNotification @@ -179,3 +188,5 @@ data UserDeletedConversationsNotification = UserDeletedConversationsNotification deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UserDeletedConversationsNotification) deriving (FromJSON, ToJSON) via (CustomEncoded UserDeletedConversationsNotification) + +instance ToSchema UserDeletedConversationsNotification diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Domain.hs b/libs/wire-api-federation/src/Wire/API/Federation/Domain.hs index 63ccb6f8ff4..2ead800ffee 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Domain.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Domain.hs @@ -17,13 +17,17 @@ module Wire.API.Federation.Domain where +import Control.Lens ((?~)) import Data.Domain (Domain) import Data.Metrics.Servant +import Data.OpenApi (OpenApi) +import Data.OpenApi qualified as S import Data.Proxy (Proxy (..)) import GHC.TypeLits (Symbol, symbolVal) import Imports import Servant.API (Header', Required, Strict, (:>)) import Servant.Client +import Servant.OpenApi (HasOpenApi (toOpenApi)) import Servant.Server import Servant.Server.Internal (MkContextWithErrorFormatter) import Wire.API.Routes.ClientAlgebra @@ -58,3 +62,9 @@ instance originDomainHeaderName :: IsString a => a originDomainHeaderName = fromString $ symbolVal (Proxy @OriginDomainHeaderName) + +instance (HasOpenApi api) => HasOpenApi (OriginDomainHeader :> api) where + toOpenApi _ = desc $ toOpenApi (Proxy @api) + where + desc :: OpenApi -> OpenApi + desc = S.allOperations . S.description ?~ ("All federated endpoints expect origin domain header: `" <> originDomainHeaderName <> "`") diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index 3d46abff3d6..e6722543b2c 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -107,6 +107,7 @@ library , servant >=0.16 , servant-client , servant-client-core + , servant-openapi3 , servant-server , singletons-th , text >=0.11 diff --git a/libs/wire-api/src/Wire/API/Asset.hs b/libs/wire-api/src/Wire/API/Asset.hs index 1658056c6d0..200fcbe245c 100644 --- a/libs/wire-api/src/Wire/API/Asset.hs +++ b/libs/wire-api/src/Wire/API/Asset.hs @@ -196,7 +196,7 @@ nilAssetKey = AssetKeyV3 (Id UUID.nil) AssetVolatile newtype AssetToken = AssetToken {assetTokenAscii :: AsciiBase64Url} deriving stock (Eq, Show) deriving newtype (FromByteString, ToByteString, Arbitrary) - deriving (FromJSON, ToJSON) via (Schema AssetToken) + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema AssetToken) instance ToSchema AssetToken where schema = diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs index 22a715678fb..646330c87b4 100644 --- a/libs/wire-api/src/Wire/API/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Conversation.hs @@ -137,7 +137,7 @@ data ConversationMetadata = ConversationMetadata } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ConversationMetadata) - deriving (FromJSON, ToJSON) via Schema ConversationMetadata + deriving (FromJSON, ToJSON, S.ToSchema) via Schema ConversationMetadata defConversationMetadata :: Maybe UserId -> ConversationMetadata defConversationMetadata mCreator = diff --git a/libs/wire-api/src/Wire/API/Conversation/Action.hs b/libs/wire-api/src/Wire/API/Conversation/Action.hs index d6930d14488..24fde8bba76 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Action.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Action.hs @@ -31,7 +31,7 @@ module Wire.API.Conversation.Action ) where -import Control.Lens ((?~)) +import Control.Lens hiding ((%~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A import Data.Aeson.KeyMap qualified as A @@ -89,6 +89,40 @@ instance ToJSON SomeConversationAction where actionJSON = fromMaybe A.Null $ schemaOut (conversationActionSchema sb) action in A.object ["tag" A..= tag, "action" A..= actionJSON] +instance S.ToSchema SomeConversationAction where + declareNamedSchema _ = do + unitSchema <- S.declareSchemaRef (Proxy :: Proxy ()) + conversationJoin <- S.declareSchemaRef (Proxy :: Proxy ConversationJoin) + conversationMemberUpdate <- S.declareSchemaRef (Proxy :: Proxy ConversationMemberUpdate) + conversationRename <- S.declareSchemaRef (Proxy :: Proxy ConversationRename) + conversationMessageTimerUpdate <- S.declareSchemaRef (Proxy :: Proxy ConversationMessageTimerUpdate) + conversationReceiptModeUpdate <- S.declareSchemaRef (Proxy :: Proxy ConversationReceiptModeUpdate) + conversationAccessData <- S.declareSchemaRef (Proxy :: Proxy ConversationAccessData) + nonEmptyListNonEmptyQualifiedUserId <- S.declareSchemaRef (Proxy :: Proxy (NonEmptyList.NonEmpty (Qualified UserId))) + protocolTag <- S.declareSchemaRef (Proxy :: Proxy ProtocolTag) + let schemas = + [ (toJSON ConversationJoinTag, conversationJoin), + (toJSON ConversationLeaveTag, unitSchema), + (toJSON ConversationMemberUpdateTag, conversationMemberUpdate), + (toJSON ConversationDeleteTag, unitSchema), + (toJSON ConversationRenameTag, conversationRename), + (toJSON ConversationMessageTimerUpdateTag, conversationMessageTimerUpdate), + (toJSON ConversationReceiptModeUpdateTag, conversationReceiptModeUpdate), + (toJSON ConversationAccessDataTag, conversationAccessData), + (toJSON ConversationRemoveMembersTag, nonEmptyListNonEmptyQualifiedUserId), + (toJSON ConversationUpdateProtocolTag, protocolTag) + ] + <&> \(t, a) -> + S.Inline $ + mempty + & S.type_ ?~ S.OpenApiObject + & S.properties . at "tag" ?~ S.Inline (mempty & S.type_ ?~ S.OpenApiString & S.enum_ ?~ [t]) + & S.properties . at "action" ?~ a + & S.required .~ ["tag", "action"] + pure $ + S.NamedSchema (Just "SomeConversationAction") $ + mempty & S.oneOf ?~ schemas + conversationActionSchema :: forall tag. Sing tag -> ValueSchema NamedSwaggerDoc (ConversationAction tag) conversationActionSchema SConversationJoinTag = schema @ConversationJoin conversationActionSchema SConversationLeaveTag = diff --git a/libs/wire-api/src/Wire/API/Conversation/Action/Tag.hs b/libs/wire-api/src/Wire/API/Conversation/Action/Tag.hs index 00e46cdfdf5..2ebac9c3605 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Action/Tag.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Action/Tag.hs @@ -22,6 +22,7 @@ module Wire.API.Conversation.Action.Tag where import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.OpenApi qualified as S import Data.Schema hiding (tag) import Data.Singletons.TH import Imports @@ -66,6 +67,9 @@ instance ToJSON ConversationActionTag where instance FromJSON ConversationActionTag where parseJSON = schemaParseJSON +instance S.ToSchema ConversationActionTag where + declareNamedSchema = schemaToSwagger + $(genSingletons [''ConversationActionTag]) $(singDecideInstance ''ConversationActionTag) diff --git a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs index 5eb4a3dc66e..71a3fc1faf9 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs @@ -53,6 +53,8 @@ data ProtocolTag = ProtocolProteusTag | ProtocolMLSTag | ProtocolMixedTag deriving stock (Eq, Show, Enum, Ord, Bounded, Generic) deriving (Arbitrary) via GenericUniform ProtocolTag +instance S.ToSchema ProtocolTag + data ConversationMLSData = ConversationMLSData { -- | The MLS group ID associated to the conversation. cnvmlsGroupId :: GroupId, @@ -158,6 +160,8 @@ deriving via (Schema Protocol) instance FromJSON Protocol deriving via (Schema Protocol) instance ToJSON Protocol +deriving via (Schema Protocol) instance S.ToSchema Protocol + protocolDataSchema :: ProtocolTag -> ObjectSchema SwaggerDoc Protocol protocolDataSchema ProtocolProteusTag = tag _ProtocolProteus (pure ()) protocolDataSchema ProtocolMLSTag = tag _ProtocolMLS mlsDataSchema diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index 59b72799992..b85428cde02 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -143,6 +143,8 @@ data GalleyError deriving (Show, Eq, Generic) deriving (FromJSON, ToJSON) via (CustomEncoded GalleyError) +instance S.ToSchema GalleyError + $(genSingletons [''GalleyError]) instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: GalleyError) where diff --git a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs index 1f358b58e61..caa69516a33 100644 --- a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs +++ b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs @@ -72,7 +72,7 @@ import Wire.Arbitrary newtype CipherSuite = CipherSuite {cipherSuiteNumber :: Word16} deriving stock (Eq, Show) deriving newtype (ParseMLS, SerialiseMLS, Arbitrary) - deriving (FromJSON, ToJSON) via Schema CipherSuite + deriving (FromJSON, ToJSON, S.ToSchema) via Schema CipherSuite instance ToSchema CipherSuite where schema = diff --git a/libs/wire-api/src/Wire/API/MLS/Epoch.hs b/libs/wire-api/src/Wire/API/MLS/Epoch.hs index a12b65179f8..117e26abd28 100644 --- a/libs/wire-api/src/Wire/API/MLS/Epoch.hs +++ b/libs/wire-api/src/Wire/API/MLS/Epoch.hs @@ -21,6 +21,7 @@ module Wire.API.MLS.Epoch where import Data.Aeson qualified as A import Data.Binary +import Data.OpenApi qualified as S import Data.Schema import Imports import Wire.API.MLS.Serialisation @@ -29,7 +30,7 @@ import Wire.Arbitrary newtype Epoch = Epoch {epochNumber :: Word64} deriving stock (Eq, Show) deriving newtype (Arbitrary, Enum, ToSchema) - deriving (A.FromJSON, A.ToJSON) via (Schema Epoch) + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via (Schema Epoch) instance ParseMLS Epoch where parseMLS = Epoch <$> parseMLS diff --git a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs index 29fde7700e2..c01aa3e2366 100644 --- a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs +++ b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs @@ -48,7 +48,7 @@ import Wire.Arbitrary -- conversation. The pair of a qualified conversation ID and a subconversation -- ID identifies globally. newtype SubConvId = SubConvId {unSubConvId :: Text} - deriving newtype (Eq, ToSchema, Ord, S.ToParamSchema, ToByteString, ToJSON, FromJSON) + deriving newtype (Eq, ToSchema, Ord, S.ToParamSchema, ToByteString, ToJSON, FromJSON, S.ToSchema) deriving stock (Generic) deriving stock (Show) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs index 72aa70f4125..02625476f34 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs @@ -18,6 +18,7 @@ module Wire.API.Routes.Public.Galley.Messaging where import Data.Id +import Data.OpenApi qualified as S import Data.SOP import Generics.SOP qualified as GSOP import Imports @@ -129,6 +130,8 @@ data MessageNotSent a instance GSOP.Generic (MessageNotSent a) +instance S.ToSchema a => S.ToSchema (MessageNotSent a) + type MessageNotSentResponses a = '[ ErrorResponse 'ConvNotFound, ErrorResponse 'BrigError.UnknownClient, diff --git a/libs/wire-api/src/Wire/API/Team/Permission.hs b/libs/wire-api/src/Wire/API/Team/Permission.hs index 49a9893b370..7ed42a29e03 100644 --- a/libs/wire-api/src/Wire/API/Team/Permission.hs +++ b/libs/wire-api/src/Wire/API/Team/Permission.hs @@ -147,6 +147,8 @@ data Perm deriving (Arbitrary) via (GenericUniform Perm) deriving (FromJSON, ToJSON) via (CustomEncoded Perm) +instance S.ToSchema Perm + permsToInt :: Set Perm -> Word64 permsToInt = Set.foldr' (\p n -> n .|. permToInt p) 0 diff --git a/libs/wire-api/src/Wire/API/User/Search.hs b/libs/wire-api/src/Wire/API/User/Search.hs index deaf7c08f4d..375e1f07dc2 100644 --- a/libs/wire-api/src/Wire/API/User/Search.hs +++ b/libs/wire-api/src/Wire/API/User/Search.hs @@ -308,7 +308,7 @@ data FederatedUserSearchPolicy | FullSearch deriving (Show, Eq, Ord, Generic, Enum, Bounded) deriving (Arbitrary) via (GenericUniform FederatedUserSearchPolicy) - deriving (ToJSON, FromJSON) via (Schema FederatedUserSearchPolicy) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema FederatedUserSearchPolicy) instance ToSchema FederatedUserSearchPolicy where schema = diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 2ce4307aecc..f133e765437 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -110,6 +110,9 @@ import Wire.API.Connection qualified as Public import Wire.API.Error import Wire.API.Error.Brig qualified as E import Wire.API.Federation.API +import Wire.API.Federation.API.Brig qualified as BrigFederationAPI +import Wire.API.Federation.API.Cargohold qualified as CargoholdFederationAPI +import Wire.API.Federation.API.Galley qualified as GalleyFederationAPI import Wire.API.Federation.Error import Wire.API.Properties qualified as Public import Wire.API.Routes.API @@ -156,7 +159,18 @@ docsAPI :: Servant.Server DocsAPI docsAPI = versionedSwaggerDocsAPI :<|> pure eventNotificationSchemas - :<|> internalEndpointsSwaggerDocsAPI "brig" 9082 BrigInternalAPI.swaggerDoc + :<|> internalEndpointsSwaggerDocsAPIs + :<|> federatedEndpointsSwaggerDocsAPIs + +federatedEndpointsSwaggerDocsAPIs :: Servant.Server FederationSwaggerDocsAPI +federatedEndpointsSwaggerDocsAPIs = + swaggerSchemaUIServer (adjustSwaggerForFederationEndpoints "brig" BrigFederationAPI.swaggerDoc) + :<|> swaggerSchemaUIServer (adjustSwaggerForFederationEndpoints "galley" GalleyFederationAPI.swaggerDoc) + :<|> swaggerSchemaUIServer (adjustSwaggerForFederationEndpoints "cargohold" CargoholdFederationAPI.swaggerDoc) + +internalEndpointsSwaggerDocsAPIs :: Servant.Server InternalEndpointsSwaggerDocsAPI +internalEndpointsSwaggerDocsAPIs = + internalEndpointsSwaggerDocsAPI "brig" 9082 BrigInternalAPI.swaggerDoc :<|> internalEndpointsSwaggerDocsAPI "cannon" 9093 CannonInternalAPI.swaggerDoc :<|> internalEndpointsSwaggerDocsAPI "cargohold" 9094 CargoholdInternalAPI.swaggerDoc :<|> internalEndpointsSwaggerDocsAPI "galley" 9095 GalleyInternalAPI.swaggerDoc diff --git a/services/brig/src/Brig/API/Public/Swagger.hs b/services/brig/src/Brig/API/Public/Swagger.hs index e17607cf8f7..c896a10f7e7 100644 --- a/services/brig/src/Brig/API/Public/Swagger.hs +++ b/services/brig/src/Brig/API/Public/Swagger.hs @@ -5,10 +5,12 @@ module Brig.API.Public.Swagger SwaggerDocsAPIBase, ServiceSwaggerDocsAPIBase, DocsAPI, + FederationSwaggerDocsAPI, pregenSwagger, swaggerPregenUIServer, eventNotificationSchemas, adjustSwaggerForInternalEndpoint, + adjustSwaggerForFederationEndpoints, emptySwagger, ) where @@ -54,7 +56,19 @@ type InternalEndpointsSwaggerDocsAPI = type NotificationSchemasAPI = "api" :> "event-notification-schemas" :> Get '[JSON] [S.Definitions S.Schema] -type DocsAPI = VersionedSwaggerDocsAPI :<|> NotificationSchemasAPI :<|> InternalEndpointsSwaggerDocsAPI +type FederationSwaggerDocsAPI = + "api-federation" + :> "swagger-ui" + :> ( ServiceSwaggerDocsAPIBase "brig" + :<|> ServiceSwaggerDocsAPIBase "galley" + :<|> ServiceSwaggerDocsAPIBase "cargohold" + ) + +type DocsAPI = + VersionedSwaggerDocsAPI + :<|> NotificationSchemasAPI + :<|> InternalEndpointsSwaggerDocsAPI + :<|> FederationSwaggerDocsAPI pregenSwagger :: Version -> Q Exp pregenSwagger v = @@ -71,7 +85,7 @@ swaggerPregenUIServer = adjustSwaggerForInternalEndpoint :: String -> PortNumber -> S.OpenApi -> S.OpenApi adjustSwaggerForInternalEndpoint service examplePort swagger = swagger - & S.info . S.title .~ T.pack ("Wire-Server internal API (" ++ service ++ ")") + & S.info . S.title .~ T.pack ("Wire-Server Internal API (" ++ service ++ ")") & S.info . S.description ?~ renderedDescription & S.allOperations . S.tags <>~ tag -- Enforce HTTP as the services themselves don't understand HTTPS @@ -97,6 +111,15 @@ adjustSwaggerForInternalEndpoint service examplePort swagger = ++ " But, the proposed `curl` commands will." ] +adjustSwaggerForFederationEndpoints :: String -> S.OpenApi -> S.OpenApi +adjustSwaggerForFederationEndpoints service swagger = + swagger + & S.info . S.title .~ T.pack ("Wire-Server Federation API (" ++ service ++ ")") + & S.allOperations . S.tags <>~ tag + where + tag :: InsOrdSet.InsOrdHashSet S.TagName + tag = InsOrdSet.singleton @S.TagName (T.pack service) + emptySwagger :: Servant.Server (ServiceSwaggerDocsAPIBase a) emptySwagger = swaggerSchemaUIServer $ diff --git a/services/nginz/integration-test/conf/nginz/nginx.conf b/services/nginz/integration-test/conf/nginz/nginx.conf index d0818f3419a..6485d34a58d 100644 --- a/services/nginz/integration-test/conf/nginz/nginx.conf +++ b/services/nginz/integration-test/conf/nginz/nginx.conf @@ -181,6 +181,16 @@ http { proxy_pass http://brig; } + location ~* ^(/v[0-9]+)?/api-federation/swagger-ui { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^(/v[0-9]+)?/api-federation/swagger.json { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + location /register { include common_response_no_zauth.conf; proxy_pass http://brig;