Skip to content

Commit

Permalink
Credential Provisioning for SFT authentication (#3915)
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanwire authored and fisx committed Apr 15, 2024
1 parent d161579 commit 10aefd6
Show file tree
Hide file tree
Showing 26 changed files with 580 additions and 174 deletions.
32 changes: 32 additions & 0 deletions changelog.d/0-release-notes/WPB-227
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
There is a new optional Boolean option, `multiSFT.enabled`, in `brig.yaml`,
allowing calls between federated SFT servers. If provided, the field
`is_federating` in the response of `/calls/config/v2` will reflect
`multiSFT.enabled`'s value.

Example:

```
# [brig.yaml]
multiSFT:
enabled: true
```

Also, the optional object `sftToken` with its fields `ttl` and `secret` define
whether an SFT credential would be rendered in the response of
`/calls/config/v2`. The field `ttl` determines the seconds for the credential to
be valid and `secret` is the path to the secret shared with SFT to create
credentials.

Example:

```
# [brig.yaml]
sft:
sftBaseDomain: sft.wire.example.com
sftSRVServiceName: sft
sftDiscoveryIntervalSeconds: 10
sftListLength: 20
sftToken:
ttl: 120
secret: /path/to/secret
```
8 changes: 8 additions & 0 deletions charts/brig/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ data:
host: gundeck
port: 8080
multiSFT: {{ .multiSFT.enabled }}
{{- if .enableFederation }}
# TODO remove this
federator:
Expand Down Expand Up @@ -209,6 +210,13 @@ data:
{{- if .sftDiscoveryIntervalSeconds }}
sftDiscoveryIntervalSeconds: {{ .sftDiscoveryIntervalSeconds }}
{{- end }}
{{- if .sftToken }}
sftToken:
{{- with .sftToken }}
ttl: {{ .ttl }}
secret: {{ .secret }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

Expand Down
2 changes: 2 additions & 0 deletions charts/brig/templates/tests/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ data:
host: spar
port: 8080
multiSFT: false
# TODO remove this
federator:
host: federator
Expand Down
2 changes: 2 additions & 0 deletions charts/brig/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ config:
# -- If set to false, 'dynamoDBEndpoint' _must_ be set.
randomPrekeys: true
useSES: true
multiSFT:
enabled: false # keep multiSFT default in sync with sft chart's multiSFT.enabled
enableFederation: false # keep enableFederation default in sync with galley and cargohold chart's config.enableFederation as well as wire-server chart's tags.federation
# Not used if enableFederation is false
rabbitmq:
Expand Down
2 changes: 1 addition & 1 deletion charts/sftd/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ turnDiscoveryEnabled: false
# Allow establishing calls involving remote SFT servers (e.g. for Federation)
# Requires appVersion 3.0.9 or later
multiSFT:
enabled: false
enabled: false # keep multiSFT default in sync with brig chart's config.multiSFT
# For sftd versions up to 3.1.3, sftd uses the TURN servers advertised at a
# discovery URL.
turnDiscoveryURL: ""
Expand Down
24 changes: 24 additions & 0 deletions docs/src/developer/reference/config-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,30 @@ This setting assumes that the sft load balancer has been deployed with the `sftd
Additionally if `setSftListAllServers` is set to `enabled` (disabled by default) then the `/calls/config/v2` endpoint will include a list of all servers that are load balanced by `setSftStaticUrl` at field `sft_servers_all`. This is required to enable calls between federated instances of Wire.
Calls between federated SFT servers can be enabled using the optional boolean `multiSFT.enabled`. If provided, the field `is_federating` in the response of `/calls/config/v2` will reflect `multiSFT.enabled`'s value.
```
# [brig.yaml]
multiSFT:
enabled: true
```
Also, the optional object `sftToken` with its fields `ttl` and `secret` define whether an SFT credential would be rendered in the response of `/calls/config/v2`. The field `ttl` determines the seconds for the credential to be valid and `secret` is the path to the secret shared with SFT to create credentials.
Example:
```
# [brig.yaml]
sft:
sftBaseDomain: sft.wire.example.com
sftSRVServiceName: sft
sftDiscoveryIntervalSeconds: 10
sftListLength: 20
sftToken:
ttl: 120
secret: /path/to/secret
```
### Locale
Expand Down
2 changes: 2 additions & 0 deletions hack/helm_vars/wire-server/values.yaml.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ brig:
accessTokenTimeout: 30
providerTokenTimeout: 60
enableFederation: true # keep in sync with galley.config.enableFederation, cargohold.config.enableFederation and tags.federator!
multiSFT:
enabled: false # keep multiSFT default in sync with brig and sft chart's config.multiSFT
optSettings:
setActivationTimeout: 10
setVerificationTimeout: 10
Expand Down
6 changes: 6 additions & 0 deletions integration/test/API/Brig.hs
Original file line number Diff line number Diff line change
Expand Up @@ -636,3 +636,9 @@ renewToken :: (HasCallStack, MakesValue uid) => uid -> String -> App Response
renewToken caller cookie = do
req <- baseRequest caller Brig Versioned "access"
submit "POST" (addHeader "Cookie" ("zuid=" <> cookie) req)

-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/get_calls_config_v2
getCallsConfigV2 :: (HasCallStack, MakesValue user) => user -> App Response
getCallsConfigV2 user = do
req <- baseRequest user Brig Versioned $ joinHttpPath ["calls", "config", "v2"]
submit "GET" req
107 changes: 106 additions & 1 deletion integration/test/Test/Brig.hs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
module Test.Brig where

import API.Brig
import qualified API.BrigInternal as BrigI
import API.Common (randomName)
import API.Common
import Data.Aeson.Types hiding ((.=))
import Data.List.Split
import Data.String.Conversions
import qualified Data.UUID as UUID
import qualified Data.UUID.V4 as UUID
import GHC.Stack
import SetupHelpers
import System.IO.Extra
import Testlib.Assertions
import Testlib.Prelude
import UnliftIO.Temporary

testCrudFederationRemotes :: HasCallStack => App ()
testCrudFederationRemotes = do
Expand Down Expand Up @@ -124,3 +128,104 @@ testCrudFederationRemoteTeams = do
l <- resp.json & asList
remoteTeams <- forM l (\e -> e %. "team_id" & asString)
when (any (\t -> t `notElem` remoteTeams) tids) $ assertFailure "Expected response to contain all of the teams"

testSFTCredentials :: HasCallStack => App ()
testSFTCredentials = do
let ttl = (60 :: Int)
withSystemTempFile "sft-secret" $ \secretFile secretHandle -> do
liftIO $ do
hPutStr secretHandle "xMtZyTpu=Leb?YKCoq#BXQR:gG^UrE83dNWzFJ2VcD"
hClose secretHandle
withModifiedBackend
( def
{ brigCfg =
( setField "sft.sftBaseDomain" "integration-tests.zinfra.io"
. setField "sft.sftToken.ttl" ttl
. setField "sft.sftToken.secret" secretFile
. setField "optSettings.setSftListAllServers" "enabled"
)
}
)
$ \domain -> do
user <- randomUser domain def
bindResponse (getCallsConfigV2 user) \resp -> do
sftServersAll <- resp.json %. "sft_servers_all" & asList
when (null sftServersAll) $ assertFailure "sft_servers_all missing"
for_ sftServersAll $ \s -> do
cred <- s %. "credential" & asString
when (null cred) $ assertFailure "credential missing"
usr <- s %. "username" & asString
let parts = splitOn "." usr
when (length parts /= 5) $ assertFailure "username should have 5 parts"
when (take 2 (head parts) /= "d=") $ assertFailure "missing expiry time identifier"
when (take 2 (parts !! 1) /= "v=") $ assertFailure "missing version identifier"
when (take 2 (parts !! 2) /= "k=") $ assertFailure "missing key ID identifier"
when (take 2 (parts !! 3) /= "s=") $ assertFailure "missing federation identifier"
when (take 2 (parts !! 4) /= "r=") $ assertFailure "missing random data identifier"
for_ parts $ \part -> when (length part < 3) $ assertFailure ("value missing for " <> part)

testSFTNoCredentials :: HasCallStack => App ()
testSFTNoCredentials = withModifiedBackend
( def
{ brigCfg =
( setField "sft.sftBaseDomain" "integration-tests.zinfra.io"
. setField "optSettings.setSftListAllServers" "enabled"
)
}
)
$ \domain -> do
user <- randomUser domain def
bindResponse (getCallsConfigV2 user) \resp -> do
sftServersAll <- resp.json %. "sft_servers_all" & asList
when (null sftServersAll) $ assertFailure "sft_servers_all missing"
for_ sftServersAll $ \s -> do
credM <- lookupField s "credential"
when (isJust credM) $ assertFailure "should not generate credential"
usrM <- lookupField s "username"
when (isJust usrM) $ assertFailure "should not generate username"

testSFTFederation :: HasCallStack => App ()
testSFTFederation = do
withModifiedBackend
( def
{ brigCfg =
( setField "sft.sftBaseDomain" "integration-tests.zinfra.io"
. removeField "multiSFT"
)
}
)
$ \domain -> do
user <- randomUser domain def
bindResponse (getCallsConfigV2 user) \resp -> do
isFederatingM <- lookupField resp.json "is_federating"
when (isJust isFederatingM) $ assertFailure "is_federating should not be present"
withModifiedBackend
( def
{ brigCfg =
( setField "sft.sftBaseDomain" "integration-tests.zinfra.io"
. setField "multiSFT" True
)
}
)
$ \domain -> do
user <- randomUser domain def
bindResponse (getCallsConfigV2 user) \resp -> do
isFederating <-
maybe (assertFailure "is_federating missing") asBool
=<< lookupField resp.json "is_federating"
unless isFederating $ assertFailure "is_federating should be true"
withModifiedBackend
( def
{ brigCfg =
( setField "sft.sftBaseDomain" "integration-tests.zinfra.io"
. setField "multiSFT" False
)
}
)
$ \domain -> do
user <- randomUser domain def
bindResponse (getCallsConfigV2 user) \resp -> do
isFederating <-
maybe (assertFailure "is_federating missing") asBool
=<< lookupField resp.json "is_federating"
when isFederating $ assertFailure "is_federating should be false"
12 changes: 7 additions & 5 deletions integration/test/Testlib/JSON.hs
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,9 @@ lookupField val selector = do
go k [] v = get v k
go k (k2 : ks) v = get v k >>= assertField v k >>= go k2 ks

-- Update nested fields
-- | Update nested fields
-- E.g. ob & "foo.bar.baz" %.= ("quux" :: String)
-- The selector path will be created if non-existing.
setField ::
forall a b.
(HasCallStack, MakesValue a, ToJSON b) =>
Expand All @@ -253,7 +254,8 @@ setField selector v x = do
member :: (HasCallStack, MakesValue a) => String -> a -> App Bool
member k x = KM.member (KM.fromString k) <$> (make x >>= asObject)

-- Update nested fields, using the old value with a stateful action
-- | Update nested fields, using the old value with a stateful action
-- The selector path will be created if non-existing.
modifyField :: (HasCallStack, MakesValue a, ToJSON b) => String -> (Maybe Value -> App b) -> a -> App Value
modifyField selector up x = do
v <- make x
Expand All @@ -268,7 +270,7 @@ modifyField selector up x = do
newValue <- toJSON <$> up (KM.lookup k' ob)
pure $ Object $ KM.insert k' newValue ob
go k (k2 : ks) v = do
val <- v %. k
val <- fromMaybe (Object $ KM.empty) <$> lookupField v k
newValue <- go k2 ks val
ob <- asObject v
pure $ Object $ KM.insert (KM.fromString k) newValue ob
Expand Down Expand Up @@ -339,9 +341,9 @@ objQid ob = do
Just v -> pure v
where
select x = runMaybeT $ do
vdom <- MaybeT $ lookupField x "domain"
vdom <- lookupFieldM x "domain"
dom <- MaybeT $ asStringM vdom
vid <- MaybeT $ lookupField x "id"
vid <- lookupFieldM x "id"
id_ <- MaybeT $ asStringM vid
pure (dom, id_)

Expand Down
Loading

0 comments on commit 10aefd6

Please sign in to comment.