Skip to content

Commit

Permalink
Multi-ingress guest links (#3546)
Browse files Browse the repository at this point in the history
  • Loading branch information
smatting authored and supersven committed Oct 4, 2023
1 parent 8c9d35d commit 23e54fe
Show file tree
Hide file tree
Showing 16 changed files with 273 additions and 92 deletions.
9 changes: 9 additions & 0 deletions charts/galley/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 14 additions & 0 deletions charts/galley/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
Expand Down
111 changes: 55 additions & 56 deletions docs/src/developer/reference/config-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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*:

Expand Down Expand Up @@ -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/
```
40 changes: 40 additions & 0 deletions integration/test/API/Galley.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
67 changes: 67 additions & 0 deletions integration/test/Test/Conversation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"
12 changes: 6 additions & 6 deletions integration/test/Testlib/HTTP.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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)}
Expand Down
3 changes: 3 additions & 0 deletions integration/test/Testlib/JSON.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion libs/wire-api/src/Wire/API/Routes/Public.hs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ module Wire.API.Routes.Public
ZAccess,
DescriptionOAuthScope,
ZHostOpt,
ZHostValue,
)
where

Expand Down Expand Up @@ -217,8 +218,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,7 @@ type ConversationAPI =
:> CanThrow 'GuestLinksDisabled
:> CanThrow 'CreateConversationCodeConflict
:> ZUser
:> ZHostOpt
:> ZOptConn
:> "conversations"
:> Capture' '[Description "Conversation ID"] "cnv" ConvId
Expand All @@ -693,6 +694,7 @@ type ConversationAPI =
:> CanThrow 'GuestLinksDisabled
:> CanThrow 'CreateConversationCodeConflict
:> ZUser
:> ZHostOpt
:> ZOptConn
:> "conversations"
:> Capture' '[Description "Conversation ID"] "cnv" ConvId
Expand Down Expand Up @@ -737,6 +739,7 @@ type ConversationAPI =
:> CanThrow 'ConvAccessDenied
:> CanThrow 'ConvNotFound
:> CanThrow 'GuestLinksDisabled
:> ZHostOpt
:> ZLocalUser
:> "conversations"
:> Capture' '[Description "Conversation ID"] "cnv" ConvId
Expand Down
Loading

0 comments on commit 23e54fe

Please sign in to comment.