diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index 10c22fafeb6..e5afcb5f428 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -59,7 +59,16 @@ data: {{- if .settings.exposeInvitationURLsTeamAllowlist }} exposeInvitationURLsTeamAllowlist: {{ .settings.exposeInvitationURLsTeamAllowlist }} {{- end }} + {{- if .settings.conversationCodeURI }} conversationCodeURI: {{ .settings.conversationCodeURI | quote }} + {{- else if .settings.multiIngress }} + multiIngress: {{- toYaml .settings.multiIngress | nindent 8 }} + {{- else }} + {{ fail "Either settings.conversationCodeURI or settings.multiIngress have to be set"}} + {{- end }} + {{- if (and .settings.conversationCodeURI .settings.multiIngress) }} + {{ fail "settings.conversationCodeURI and settings.multiIngress are mutually exclusive" }} + {{- end }} federationDomain: {{ .settings.federationDomain }} {{- if $.Values.secrets.mlsPrivateKeys }} mlsPrivateKeyPaths: diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index 875864142ff..5b328a3e5b1 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -34,6 +34,20 @@ config: exposeInvitationURLsTeamAllowlist: [] maxConvSize: 500 intraListing: true + # Either `conversationCodeURI` or `multiIngress` must be set + # + # `conversationCodeURI` is the URI prefix for conversation invitation links + # It should be of form https://{ACCOUNT_PAGES}/conversation-join/ + conversationCodeURI: null + # + # `multiIngress` is a `Z-Host` depended setting of conversationCodeURI. + # Use this only if you want to expose the instance on mutliple ingresses. + # If set it must a map from `Z-Host` to URI prefix + # Example: + # multiIngress: + # example.com: https://accounts.example.com/conversation-join/ + # example.net: https://accounts.example.net/conversation-join/ + multiIngress: null # Disable one ore more API versions. Please make sure the configuration value is the same in all these charts: # brig, cannon, cargohold, galley, gundeck, proxy, spar. # disabledAPIVersions: [ v3 ] diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 3c681740f7b..3d9dc14c4f4 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -7,8 +7,6 @@ Fragment. This page is about the yaml files that determine the configuration of the Wire backend services. -## Settings in galley - ### MLS private key paths Note: This developer documentation. Documentation for site operators can be found here: {ref}`mls-message-layer-security` @@ -657,39 +655,71 @@ The default setting is that no API version is disabled. ## Settings in cargohold -### (Fake) AWS - AWS S3 (or an alternative provider / service) is used to upload and download assets. The Haddock of [`CargoHold.Options.AWSOpts`](https://github.com/wireapp/wire-server/blob/develop/services/cargohold/src/CargoHold/Options.hs#L64) provides a lot of useful information. -#### Multi-Ingress setup + +## Multi-Ingress setup In a multi-ingress setup the backend is reachable via several domains, each handled by a separate Kubernetes ingress. This is useful to obfuscate the relationship of clients to each other, as an attacker on TCP/IP-level could only see domains and IPs that do not obviously relate to each other. +Each of these backend domains represents a virtual backend. N.B. these backend +domains are *DNS domains* only, not to be confused of the "backend domain" term used for federation (see {ref}`configure-federation`). In single-ingress setups the backend DNS domain and federation backend domain is usually be the same, but this is not true for multi-ingress setups. + + +For a multi-ingress setup multiple services need to be configured: +### Nginz + +nginz sets [CORS +headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). To generate +them for multiple domains (usually, *nginz* works with only one root domain) +these need to be defined with `nginx_conf.additional_external_env_domains`. + +E.g. + +```yaml +nginx_conf: + additional_external_env_domains: + - red.example.com + - green.example.org + - blue.example.net +``` + +### Cannon + +*cannon* sets [CORS +headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) for direct API +accesses by clients. To generate them for multiple domains (usually, *cannon* +works with only one root domain) these need to be defined with +`nginx_conf.additional_external_env_domains`. + +E.g. + +```yaml +nginx_conf: + additional_external_env_domains: + - red.example.com + - green.example.org + - blue.example.net +``` + +### Cargohold -In case of a fake AWS S3 service its identity needs to be obfuscated by making -it accessible via several domains, too. Thus, there isn't one -`s3DownloadEndpoint`, but one per domain at which the backend is reachable. Each -of these backend domains represents a virtual backend. N.B. these backend -domains are *DNS domains*. Do not confuse them with the federation domain! The -latter is just an identifier, and may or may not be equal to the backend's DNS -domain. Backend DNS domain(s) and federation domain are usually set equal by -convention. But, this is not true for multi-ingress setups! The backend domain of a download request is defined by its `Z-Host` header which -is set by `nginz`. (Multi-ingress handlling only applies to download requests as +is set by `nginz`. Multi-ingress handling only applies to download requests as these are implemented by redirects to the S3 assets host for local assets. -Uploads are handled by cargohold directly itself.) +Uploads are handled by cargohold directly itself. + -The config `aws.multiIngress` is a map from backend domain (`Z-Host` header -value) to a S3 download endpoint. The `Z-Host` header is set by `nginz` to the +For a multi-ingress setup `aws.multiIngress` needs to be configured as a map from backend domain (`Z-Host` header value) to a S3 download endpoint. The `Z-Host` header is set by `nginz` to the value of the incoming requests `Host` header. If there's no config map entry for a provided `Z-Host` in a download request for a local asset, then an error is -returned. +returned. When configured the configuration of `s3DownloadEndpoint` is ignored. This example shows a setup with fake backends *red*, *green* and *blue*: @@ -724,45 +754,14 @@ Link to diagram: https://mermaid.live/edit#pako:eNrdVbFu2zAQ_ZUDJ7ewDdhtUkBDgBRB0CHIYCNL4eVEnmWiMk8lKbttkH8vJbsW5dCOUXSqBkHiPT6-e3yinoVkRSITEC5H32syku40FhbXCwP7C6VnC1hqSQNL6l1XeWRPwBuKqxk8OXKwpRyrahxGxvQD11VJY8mvSHPOB4UlMknSrtonbcfStBVar6Wu0HjQJgCdGwUNKfaonMGMax8WeH9acIq5FXKOuwVE7BcqN4U2v9IlibbgFZcqXZ5_ABeMxYK6uiXpwRb5YHp1NYTJ9FN7ixw3jW6ri5UHXva28rZ5BsVbUzIqB-gc-WgTD9DRzU3Pz7v9FChZYnk8L4KGiW23Gdyz3aJVQW7IoYvQbT3gDq2_wsIIbpWCr6MvHF5WhIpsL2p6g6HFhHePvdajFR6Yv0Fd7ZTDquF9mj3AMoR2t0zHcZg1CiJj92akdGP-OLBJ9JpDFOa73YGNxnRAFZ3Te9rxey5L3gZHdmueMrsLyBnHDwpScerGQr_9dn1tzfFeR_2k2MioRFIn15MhTD82Sb0-ndT4fPjM-emcdsDItf23eVlSW_D_ltXYv0uzenTknU_rOd_fzOsfy_9xYvtN_21ixVCsya5Rq_D3fG6KC-FXtKaFyMKjoiXWpV-IhXkJUKw9z38aKTJvaxqKulKBff-jFdkSS0cvvwHKl250 --> -## Settings in cannon +### Galley -### Multi-Ingress setup - -*cannon* sets [CORS -headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) for direct API -accesses by clients. To generate them for multiple domains (usually, *cannon* -works with only one root domain) these need to be defined with -`nginx_conf.additional_external_env_domains`. +For conversation invite links to be correct in a multi-ingress setup `settings.multiIngress` needs to be configured as map from `Z-Host` to the conversation URI prefix. This setting is a `Z-Host` depended version of `settings.conversationCodeURI`. In fact `settings.multiIngress` and `settings.conversationCodeURI` are mutually exclusive. -E.g. +Example: ```yaml -nginx_conf: - additional_external_env_domains: - - red.example.com - - green.example.org - - blue.example.net -``` - -This setting has a dual in the *nginz* configuration. - -## Settings in nginz - -### Multi-Ingress setup - -nginz sets [CORS -headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). To generate -them for multiple domains (usually, *nginz* works with only one root domain) -these need to be defined with `nginx_conf.additional_external_env_domains`. - -E.g. - -```yaml -nginx_conf: - additional_external_env_domains: - - red.example.com - - green.example.org - - blue.example.net -``` - -This setting has a dual in the *cannon* configuration. +multiIngress: + red.example.com: https://accounts.red.example.com/conversation-join/ + green.example.com: https://accounts.green.example.net/conversation-join/ +``` \ No newline at end of file diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 967786f86c0..bb3aa2ac0d5 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -42,6 +42,13 @@ defProteus = defMLS :: CreateConv defMLS = defProteus {protocol = "mls"} +allowGuests :: CreateConv -> CreateConv +allowGuests cc = + cc + { access = Just ["code"], + accessRole = Just ["team_member", "guest"] + } + instance MakesValue CreateConv where make cc = do quids <- for (cc.qualifiedUsers) objQidObject @@ -223,3 +230,36 @@ removeMember remover qcnv removed = do (removedDomain, removedId) <- objQid removed req <- baseRequest remover Galley Versioned (joinHttpPath ["conversations", convDomain, convId, "members", removedDomain, removedId]) submit "DELETE" req + +postConversationCode :: + (HasCallStack, MakesValue user, MakesValue conv) => + user -> + conv -> + Maybe String -> + Maybe String -> + App Response +postConversationCode user conv mbpassword mbZHost = do + convId <- objId conv + req <- baseRequest user Galley Versioned (joinHttpPath ["conversations", convId, "code"]) + submit + "POST" + ( req + & addJSONObject ["password" .= pw | pw <- maybeToList mbpassword] + & maybe id zHost mbZHost + ) + +getConversationCode :: + (HasCallStack, MakesValue user, MakesValue conv) => + user -> + conv -> + Maybe String -> + App Response +getConversationCode user conv mbZHost = do + convId <- objId conv + req <- baseRequest user Galley Versioned (joinHttpPath ["conversations", convId, "code"]) + submit + "GET" + ( req + & addQueryParams [("cnv", convId)] + & maybe id zHost mbZHost + ) diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index ef1ef920c38..f956480dc85 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -11,6 +11,7 @@ import API.Gundeck (getNotifications) import Control.Applicative import Control.Concurrent (threadDelay) import Data.Aeson qualified as Aeson +import Data.Text qualified as T import GHC.Stack import SetupHelpers import Testlib.One2One (generateRemoteAndConvIdWithDomain) @@ -462,3 +463,69 @@ testGetOneOnOneConvInStatusSentFromRemote = do filter ((==) d2ConvId) qConvIds `shouldMatch` [d2ConvId] resp <- getConversation d1User d2ConvId resp.status `shouldMatchInt` 200 + +testMultiIngressGuestLinks :: HasCallStack => App () +testMultiIngressGuestLinks = do + do + configuredURI <- readServiceConfig Galley & (%. "settings.conversationCodeURI") & asText + + (user, _) <- createTeam OwnDomain + conv <- postConversation user (allowGuests defProteus) >>= getJSON 201 + + bindResponse (postConversationCode user conv Nothing Nothing) $ \resp -> do + res <- getJSON 201 resp + res %. "type" `shouldMatch` "conversation.code-update" + guestLink <- res %. "data.uri" & asText + assertBool "guestlink incorrect" $ configuredURI `T.isPrefixOf` guestLink + + bindResponse (getConversationCode user conv Nothing) $ \resp -> do + res <- getJSON 200 resp + guestLink <- res %. "uri" & asText + assertBool "guestlink incorrect" $ configuredURI `T.isPrefixOf` guestLink + + bindResponse (getConversationCode user conv (Just "red.example.com")) $ \resp -> do + res <- getJSON 200 resp + guestLink <- res %. "uri" & asText + assertBool "guestlink incorrect" $ configuredURI `T.isPrefixOf` guestLink + + withModifiedBackend + ( def + { galleyCfg = \conf -> + conf + & setField "settings.conversationCodeURI" Null + & setField + "settings.multiIngress" + ( object + [ "red.example.com" .= "https://red.example.com", + "blue.example.com" .= "https://blue.example.com" + ] + ) + } + ) + $ \domain -> do + (user, _) <- createTeam domain + conv <- postConversation user (allowGuests defProteus) >>= getJSON 201 + + bindResponse (postConversationCode user conv Nothing (Just "red.example.com")) $ \resp -> do + res <- getJSON 201 resp + res %. "type" `shouldMatch` "conversation.code-update" + guestLink <- res %. "data.uri" & asText + assertBool "guestlink incorrect" $ (fromString "https://red.example.com") `T.isPrefixOf` guestLink + + bindResponse (getConversationCode user conv (Just "red.example.com")) $ \resp -> do + res <- getJSON 200 resp + guestLink <- res %. "uri" & asText + assertBool "guestlink incorrect" $ (fromString "https://red.example.com") `T.isPrefixOf` guestLink + + bindResponse (getConversationCode user conv (Just "blue.example.com")) $ \resp -> do + res <- getJSON 200 resp + guestLink <- res %. "uri" & asText + assertBool "guestlink incorrect" $ (fromString "https://blue.example.com") `T.isPrefixOf` guestLink + + bindResponse (getConversationCode user conv Nothing) $ \resp -> do + res <- getJSON 403 resp + res %. "label" `shouldMatch` "access-denied" + + bindResponse (getConversationCode user conv (Just "unknown.example.com")) $ \resp -> do + res <- getJSON 403 resp + res %. "label" `shouldMatch` "access-denied" diff --git a/integration/test/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index 1aa0b80ca75..670fbdc679e 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -78,12 +78,6 @@ addQueryParams :: [(String, String)] -> HTTP.Request -> HTTP.Request addQueryParams params req = HTTP.setQueryString (map (\(k, v) -> (cs k, Just (cs v))) params) req -zType :: String -> HTTP.Request -> HTTP.Request -zType = addHeader "Z-Type" - -zHost :: String -> HTTP.Request -> HTTP.Request -zHost = addHeader "Z-Host" - contentTypeJSON :: HTTP.Request -> HTTP.Request contentTypeJSON = addHeader "Content-Type" "application/json" @@ -156,6 +150,12 @@ zConnection = addHeader "Z-Connection" zClient :: String -> HTTP.Request -> HTTP.Request zClient = addHeader "Z-Client" +zType :: String -> HTTP.Request -> HTTP.Request +zType = addHeader "Z-Type" + +zHost :: String -> HTTP.Request -> HTTP.Request +zHost = addHeader "Z-Host" + submit :: String -> HTTP.Request -> App Response submit method req0 = do let req = req0 {HTTP.method = T.encodeUtf8 (T.pack method)} diff --git a/integration/test/Testlib/JSON.hs b/integration/test/Testlib/JSON.hs index a5c932d8741..984a536ebe7 100644 --- a/integration/test/Testlib/JSON.hs +++ b/integration/test/Testlib/JSON.hs @@ -73,6 +73,9 @@ asString x = (String s) -> pure (T.unpack s) v -> assertFailureWithJSON x ("String" `typeWasExpectedButGot` v) +asText :: HasCallStack => MakesValue a => a -> App T.Text +asText = (fmap T.pack) . asString + asStringM :: HasCallStack => MakesValue a => a -> App (Maybe String) asStringM x = make x >>= \case diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index 8ad302b6dd8..9d4e8eda43c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -32,6 +32,7 @@ module Wire.API.Routes.Public ZProvider, DescriptionOAuthScope, ZHostOpt, + ZHostValue, ) where @@ -191,8 +192,10 @@ type ZOptConn = ZAuthServant 'ZAuthConn '[Servant.Optional, Servant.Strict] -- | Optional @Z-Host@ header (added by @nginz@) data ZHostOpt +type ZHostValue = Text + type ZOptHostHeader = - Header' '[Servant.Optional, Strict] "Z-Host" Text + Header' '[Servant.Optional, Strict] "Z-Host" ZHostValue instance HasSwagger api => HasSwagger (ZHostOpt :> api) where toSwagger _ = toSwagger (Proxy @api) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index ce778e7d62e..9b0b1250937 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -675,6 +675,7 @@ type ConversationAPI = :> CanThrow 'GuestLinksDisabled :> CanThrow 'CreateConversationCodeConflict :> ZUser + :> ZHostOpt :> ZOptConn :> "conversations" :> Capture' '[Description "Conversation ID"] "cnv" ConvId @@ -693,6 +694,7 @@ type ConversationAPI = :> CanThrow 'GuestLinksDisabled :> CanThrow 'CreateConversationCodeConflict :> ZUser + :> ZHostOpt :> ZOptConn :> "conversations" :> Capture' '[Description "Conversation ID"] "cnv" ConvId @@ -737,6 +739,7 @@ type ConversationAPI = :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvNotFound :> CanThrow 'GuestLinksDisabled + :> ZHostOpt :> ZLocalUser :> "conversations" :> Capture' '[Description "Conversation ID"] "cnv" ConvId diff --git a/services/cargohold/src/CargoHold/Options.hs b/services/cargohold/src/CargoHold/Options.hs index 4f99c8300ad..aa515729a1f 100644 --- a/services/cargohold/src/CargoHold/Options.hs +++ b/services/cargohold/src/CargoHold/Options.hs @@ -104,7 +104,7 @@ data AWSOpts = AWSOpts -- -- This logic is: If the @Z-Host@ header is provided and found in this map, -- the map's values is taken as s3 download endpoint to redirect to; - -- otherwise, `_awsS3DownloadEndpoint` is used. This option is only useful + -- otherwise a 404 is retuned. This option is only useful -- in the context of multi-ingress setups where one backend / deployment is -- reachable under several domains. _multiIngress :: !(Maybe (Map String AWSEndpoint)) diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index eb3253d10a3..595a89d8858 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -79,6 +79,7 @@ import Data.Id import Data.Json.Util import Data.List1 import Data.Map.Strict qualified as Map +import Data.Misc (HttpsUrl) import Data.Qualified import Data.Set qualified as Set import Data.Singletons @@ -135,6 +136,7 @@ import Wire.API.Federation.Error import Wire.API.Message import Wire.API.Password (mkSafePassword) import Wire.API.Provider.Service (ServiceRef) +import Wire.API.Routes.Public (ZHostValue) import Wire.API.Routes.Public.Galley.Messaging import Wire.API.Routes.Public.Util (UpdateResult (..)) import Wire.API.ServantProto (RawProto (..)) @@ -497,11 +499,12 @@ addCodeUnqualifiedWithReqBody :: Member TeamFeatureStore r ) => UserId -> + Maybe Text -> Maybe ConnId -> ConvId -> CreateConversationCodeRequest -> Sem r AddCodeResult -addCodeUnqualifiedWithReqBody usr mZcon cnv req = addCodeUnqualified (Just req) usr mZcon cnv +addCodeUnqualifiedWithReqBody usr mbZHost mZcon cnv req = addCodeUnqualified (Just req) usr mbZHost mZcon cnv addCodeUnqualified :: forall r. @@ -521,13 +524,14 @@ addCodeUnqualified :: ) => Maybe CreateConversationCodeRequest -> UserId -> + Maybe ZHostValue -> Maybe ConnId -> ConvId -> Sem r AddCodeResult -addCodeUnqualified mReq usr mZcon cnv = do +addCodeUnqualified mReq usr mbZHost mZcon cnv = do lusr <- qualifyLocal usr lcnv <- qualifyLocal cnv - addCode lusr mZcon lcnv mReq + addCode lusr mbZHost mZcon lcnv mReq addCode :: forall r. @@ -545,37 +549,34 @@ addCode :: Member (Embed IO) r ) => Local UserId -> + Maybe ZHostValue -> Maybe ConnId -> Local ConvId -> Maybe CreateConversationCodeRequest -> Sem r AddCodeResult -addCode lusr mZcon lcnv mReq = do +addCode lusr mbZHost mZcon lcnv mReq = do conv <- E.getConversation (tUnqualified lcnv) >>= noteS @'ConvNotFound Query.ensureGuestLinksEnabled (Data.convTeam conv) Query.ensureConvAdmin (Data.convLocalMembers conv) (tUnqualified lusr) ensureAccess conv CodeAccess ensureGuestsOrNonTeamMembersAllowed conv - let (bots, users) = localBotsAndUsers $ Data.convLocalMembers conv + convUri <- getConversationCodeURI mbZHost key <- E.makeKey (tUnqualified lcnv) - mCode <- E.getCode key ReusableCode - case mCode of + E.getCode key ReusableCode >>= \case Nothing -> do code <- E.generateCode (tUnqualified lcnv) ReusableCode (Timeout 3600 * 24 * 365) -- one year FUTUREWORK: configurable - mPw <- forM ((.password) =<< mReq) mkSafePassword + mPw <- for (mReq >>= (.password)) mkSafePassword E.createCode code mPw now <- input - conversationCode <- createCode (isJust mPw) code - let event = Event (tUntagged lcnv) Nothing (tUntagged lusr) now (EdConvCodeUpdate conversationCode) + let event = Event (tUntagged lcnv) Nothing (tUntagged lusr) now (EdConvCodeUpdate (mkConversationCodeInfo (isJust mPw) (codeKey code) (codeValue code) convUri)) + let (bots, users) = localBotsAndUsers $ Data.convLocalMembers conv pushConversationEvent mZcon event (qualifyAs lusr (map lmId users)) bots pure $ CodeAdded event + -- In case conversation already has a code this case covers the allowed no-ops Just (code, mPw) -> do when (isJust mPw || isJust (mReq >>= (.password))) $ throwS @'CreateConversationCodeConflict - conversationCode <- createCode (isJust mPw) code - pure $ CodeAlreadyExisted conversationCode + pure $ CodeAlreadyExisted (mkConversationCodeInfo (isJust mPw) (codeKey code) (codeValue code) convUri) where - createCode :: Bool -> Code -> Sem r ConversationCodeInfo - createCode hasPw code = do - mkConversationCodeInfo hasPw (codeKey code) (codeValue code) <$> E.getConversationCodeURI ensureGuestsOrNonTeamMembersAllowed :: Data.Conversation -> Sem r () ensureGuestsOrNonTeamMembersAllowed conv = unless @@ -639,10 +640,11 @@ getCode :: Member (Input Opts) r, Member TeamFeatureStore r ) => + Maybe ZHostValue -> Local UserId -> ConvId -> Sem r ConversationCodeInfo -getCode lusr cnv = do +getCode mbZHost lusr cnv = do conv <- E.getConversation cnv >>= noteS @'ConvNotFound Query.ensureGuestLinksEnabled (Data.convTeam conv) @@ -650,7 +652,8 @@ getCode lusr cnv = do ensureConvMember (Data.convLocalMembers conv) (tUnqualified lusr) key <- E.makeKey cnv (c, mPw) <- E.getCode key ReusableCode >>= noteS @'CodeNotFound - mkConversationCodeInfo (isJust mPw) (codeKey c) (codeValue c) <$> E.getConversationCodeURI + convUri <- getConversationCodeURI mbZHost + pure $ mkConversationCodeInfo (isJust mPw) (codeKey c) (codeValue c) convUri checkReusableCode :: forall r. @@ -1618,3 +1621,15 @@ rmBot lusr zcon b = do ensureConvMember :: (Member (ErrorS 'ConvNotFound) r) => [LocalMember] -> UserId -> Sem r () ensureConvMember users usr = unless (usr `isMember` users) $ throwS @'ConvNotFound + +getConversationCodeURI :: + ( Member (ErrorS 'ConvAccessDenied) r, + Member CodeStore r + ) => + Maybe ZHostValue -> + Sem r HttpsUrl +getConversationCodeURI mbZHost = do + mbURI <- E.getConversationCodeURI mbZHost + case mbURI of + Just uri -> pure uri + Nothing -> throwS @'ConvAccessDenied diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 55c1dfab8df..2163f9a8a50 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -21,8 +21,8 @@ module Galley.App ( -- * Environment Env, reqId, - monitor, options, + monitor, applog, manager, federator, @@ -53,6 +53,7 @@ import Control.Lens hiding ((.=)) import Data.Default (def) import Data.List.NonEmpty qualified as NE import Data.Metrics.Middleware +import Data.Misc import Data.Qualified import Data.Range import Data.Text (unpack) @@ -128,7 +129,7 @@ type GalleyEffects0 = type GalleyEffects = Append GalleyEffects1 GalleyEffects0 -- Define some invariants for the options used -validateOptions :: Opts -> IO () +validateOptions :: Opts -> IO (Either HttpsUrl (Map Text HttpsUrl)) validateOptions o = do let settings' = view settings o optFanoutLimit = fromIntegral . fromRange $ currentFanoutLimit o @@ -140,19 +141,26 @@ validateOptions o = do (Nothing, Just _) -> error "RabbitMQ config is specified and federator is not, please specify both or none" (Just _, Nothing) -> error "Federator is specified and RabbitMQ config is not, please specify both or none" _ -> pure () + let errMsg = "Either conversationCodeURI or multiIngress needs to be set." + case (settings' ^. conversationCodeURI, settings' ^. multiIngress) of + (Nothing, Nothing) -> error errMsg + (Nothing, Just mi) -> pure (Right mi) + (Just uri, Nothing) -> pure (Left uri) + (Just _, Just _) -> error errMsg createEnv :: Metrics -> Opts -> Logger -> IO Env createEnv m o l = do cass <- initCassandra o l mgr <- initHttpManager o h2mgr <- initHttp2Manager - validateOptions o + codeURIcfg <- validateOptions o Env def m o l mgr h2mgr (o ^. O.federator) (o ^. O.brig) cass <$> Q.new 16000 <*> initExtEnv <*> maybe (pure Nothing) (fmap Just . Aws.mkEnv l mgr) (o ^. journal) <*> loadAllMLSKeys (fold (o ^. settings . mlsPrivateKeyPaths)) <*> traverse (mkRabbitMqChannelMVar l) (o ^. rabbitmq) + <*> pure codeURIcfg initCassandra :: Opts -> Logger -> IO ClientState initCassandra o l = do diff --git a/services/galley/src/Galley/Cassandra/Code.hs b/services/galley/src/Galley/Cassandra/Code.hs index 9206425afe3..f5b2770b38a 100644 --- a/services/galley/src/Galley/Cassandra/Code.hs +++ b/services/galley/src/Galley/Cassandra/Code.hs @@ -23,13 +23,13 @@ where import Cassandra import Control.Lens import Data.Code +import Data.Map qualified as Map import Galley.Cassandra.Queries qualified as Cql import Galley.Cassandra.Store import Galley.Data.Types import Galley.Data.Types qualified as Code import Galley.Effects.CodeStore (CodeStore (..)) import Galley.Env -import Galley.Options import Imports import Polysemy import Polysemy.Input @@ -48,8 +48,14 @@ interpretCodeStoreToCassandra = interpret $ \case DeleteCode k s -> embedClient $ deleteCode k s MakeKey cid -> Code.mkKey cid GenerateCode cid s t -> Code.generate cid s t - GetConversationCodeURI -> - view (options . settings . conversationCodeURI) <$> input + GetConversationCodeURI mbHost -> do + env <- input + case env ^. convCodeURI of + Left uri -> pure (Just uri) + Right map' -> + case mbHost of + Just host -> pure (Map.lookup host map') + Nothing -> pure Nothing -- | Insert a conversation code insertCode :: Code -> Maybe Password -> Client () diff --git a/services/galley/src/Galley/Effects/CodeStore.hs b/services/galley/src/Galley/Effects/CodeStore.hs index 88b31b0dfc1..15d71162f3b 100644 --- a/services/galley/src/Galley/Effects/CodeStore.hs +++ b/services/galley/src/Galley/Effects/CodeStore.hs @@ -53,6 +53,6 @@ data CodeStore m a where DeleteCode :: Key -> Scope -> CodeStore m () MakeKey :: ConvId -> CodeStore m Key GenerateCode :: ConvId -> Scope -> Timeout -> CodeStore m Code - GetConversationCodeURI :: CodeStore m HttpsUrl + GetConversationCodeURI :: Maybe Text -> CodeStore m (Maybe HttpsUrl) makeSem ''CodeStore diff --git a/services/galley/src/Galley/Env.hs b/services/galley/src/Galley/Env.hs index 32e45f2aa73..2bdb38c27ff 100644 --- a/services/galley/src/Galley/Env.hs +++ b/services/galley/src/Galley/Env.hs @@ -25,7 +25,7 @@ import Control.Lens hiding ((.=)) import Data.ByteString.Conversion (toByteString') import Data.Id import Data.Metrics.Middleware -import Data.Misc (Fingerprint, Rsa) +import Data.Misc (Fingerprint, HttpsUrl, Rsa) import Data.Range import Galley.Aws qualified as Aws import Galley.Options @@ -63,7 +63,8 @@ data Env = Env _extEnv :: ExtEnv, _aEnv :: Maybe Aws.Env, _mlsKeys :: SignaturePurpose -> MLSKeys, - _rabbitmqChannel :: Maybe (MVar Q.Channel) + _rabbitmqChannel :: Maybe (MVar Q.Channel), + _convCodeURI :: Either HttpsUrl (Map Text HttpsUrl) } -- | Environment specific to the communication with external diff --git a/services/galley/src/Galley/Options.hs b/services/galley/src/Galley/Options.hs index ab34df7f996..3ec6ba07668 100644 --- a/services/galley/src/Galley/Options.hs +++ b/services/galley/src/Galley/Options.hs @@ -22,8 +22,8 @@ module Galley.Options httpPoolSize, maxTeamSize, maxFanoutSize, - exposeInvitationURLsTeamAllowlist, maxConvSize, + exposeInvitationURLsTeamAllowlist, intraListing, disabledAPIVersions, conversationCodeURI, @@ -32,6 +32,7 @@ module Galley.Options federationDomain, mlsPrivateKeyPaths, featureFlags, + multiIngress, defConcurrentDeletionEvents, defDeleteConvThrottleMillis, defFanoutLimit, @@ -90,7 +91,19 @@ data Settings = Settings -- | Whether to call Brig for device listing _intraListing :: !Bool, -- | URI prefix for conversations with access mode @code@ - _conversationCodeURI :: !HttpsUrl, + _conversationCodeURI :: !(Maybe HttpsUrl), + -- | Map from @Z-Host@ header to URI prefix for conversations with access mode @code@ + -- + -- If setMultiIngress is set then the URI prefix for guest links is looked + -- up in this config setting using the @Z-Host@ header value as a key. If + -- the lookup fails then no guest link can be created via the API. + -- + -- This option is only useful in the context of multi-ingress setups where + -- one backend / deployment is is reachable under several domains. + -- + -- multiIngress and conversationCodeURI are mutually exclusive. One of + -- both options need to be configured. + _multiIngress :: Maybe (Map Text HttpsUrl), -- | Throttling: limits to concurrent deletion events _concurrentDeletionEvents :: !(Maybe Int), -- | Throttling: delay between sending events upon team deletion