Skip to content

Commit

Permalink
crl proxy flag
Browse files Browse the repository at this point in the history
  • Loading branch information
battermann committed May 29, 2024
1 parent 4a09672 commit 856d1f6
Show file tree
Hide file tree
Showing 25 changed files with 247 additions and 24 deletions.
2 changes: 2 additions & 0 deletions cassandra-schema.cql
Original file line number Diff line number Diff line change
Expand Up @@ -1205,9 +1205,11 @@ CREATE TABLE galley_test.team_features (
mls_default_ciphersuite int,
mls_default_protocol int,
mls_e2eid_acme_discovery_url blob,
mls_e2eid_crl_proxy blob,
mls_e2eid_grace_period int,
mls_e2eid_lock_status int,
mls_e2eid_status int,
mls_e2eid_use_proxy_on_mobile boolean,
mls_e2eid_ver_exp timestamp,
mls_lock_status int,
mls_migration_finalise_regardless_after timestamp,
Expand Down
1 change: 1 addition & 0 deletions changelog.d/2-features/WPB-8824
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Updated the `mlsE2EId` feature config with two additional fields `crlProxy` and `useProxyOnMobile`
6 changes: 6 additions & 0 deletions docs/src/developer/reference/config-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,10 @@ When a client first tries to fetch or renew a certificate, they may need to logi

The client enrolls using the Automatic Certificate Management Environment (ACME) protocol [RFC 8555](https://www.rfc-editor.org/rfc/rfc8555.html). The `acmeDiscoveryUrl` parameter must be set to the HTTPS URL of the ACME server discovery endpoint for this team. It is of the form "https://acme.{backendDomain}/acme/{provisionerName}/discovery". For example: `https://acme.example.com/acme/provisioner1/discovery`.

`useProxyOnMobile` is an optional field. If `true`, mobile clients should use the CRL proxy. If missing, null or false, mobile clients should not use the CRL proxy.

`crlProxy` contains the URL to the CRL proxy. (Not that this field is optional in the server config, but mandatory when the team feature is updated via the team feature API.)

```yaml
# galley.yaml
mlsE2EId:
Expand All @@ -342,6 +346,8 @@ mlsE2EId:
config:
verificationExpiration: 86400
acmeDiscoveryUrl: null
useProxyOnMobile: true
crlProxy: https://example.com
lockStatus: unlocked
```

Expand Down
6 changes: 6 additions & 0 deletions docs/src/understand/team-feature-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ When a client first tries to fetch or renew a certificate, they may need to logi

The client enrolls using the Automatic Certificate Management Environment (ACME) protocol [RFC 8555](https://www.rfc-editor.org/rfc/rfc8555.html). The `acmeDiscoveryUrl` parameter must be set to the HTTPS URL of the ACME server discovery endpoint for this team. It is of the form "https://acme.{backendDomain}/acme/{provisionerName}/discovery". For example: `https://acme.example.com/acme/provisioner1/discovery`.

`useProxyOnMobile` is an optional field. If `true`, mobile clients should use the CRL proxy. If missing, null or false, mobile clients should not use the CRL proxy.

`crlProxy` contains the URL to the CRL proxy. (Not that this field is optional in the server config, but mandatory when the team feature is updated via the team feature API.)

```yaml
galley:
# ...
Expand All @@ -109,6 +113,8 @@ galley:
config:
verificationExpiration: 86400
acmeDiscoveryUrl: null
useProxyOnMobile: true
crlProxy: https://example.com
lockStatus: unlocked
```

Expand Down
19 changes: 19 additions & 0 deletions integration/test/API/Galley.hs
Original file line number Diff line number Diff line change
Expand Up @@ -571,3 +571,22 @@ getLegalHoldStatus tid zusr = do
uidStr <- asString $ zusr %. "id"
req <- baseRequest zusr Galley Versioned (joinHttpPath ["teams", tidStr, "legalhold", uidStr])
submit "GET" req

-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/get_feature_configs
getFeatureConfigs :: (HasCallStack, MakesValue user) => user -> App Response
getFeatureConfigs user = do
req <- baseRequest user Galley Versioned "/feature-configs"
submit "GET" req

-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/get_teams__tid__features
getTeamFeatures :: (HasCallStack, MakesValue user, MakesValue tid) => user -> tid -> App Response
getTeamFeatures user tid = do
tidStr <- asString tid
req <- baseRequest user Galley Versioned (joinHttpPath ["teams", tidStr, "features"])
submit "GET" req

getTeamFeature :: (HasCallStack, MakesValue user, MakesValue tid) => user -> tid -> String -> App Response
getTeamFeature user tid featureName = do
tidStr <- asString tid
req <- baseRequest user Galley Versioned (joinHttpPath ["teams", tidStr, "features", featureName])
submit "GET" req
13 changes: 10 additions & 3 deletions integration/test/API/GalleyInternal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,11 @@ getTeamFeature domain_ featureName tid = do
req <- baseRequest domain_ Galley Unversioned $ joinHttpPath ["i", "teams", tid, "features", featureName]
submit "GET" $ req

setTeamFeatureStatus :: (HasCallStack, MakesValue domain, MakesValue team) => domain -> team -> String -> String -> App ()
setTeamFeatureStatus :: (HasCallStack, MakesValue domain, MakesValue team) => domain -> team -> String -> String -> App Response
setTeamFeatureStatus domain team featureName status = do
tid <- asString team
req <- baseRequest domain Galley Unversioned $ joinHttpPath ["i", "teams", tid, "features", featureName]
res <- submit "PATCH" $ req & addJSONObject ["status" .= status]
res.status `shouldMatchInt` 200
submit "PATCH" $ req & addJSONObject ["status" .= status]

getFederationStatus ::
( HasCallStack,
Expand Down Expand Up @@ -70,3 +69,11 @@ legalholdIsTeamInWhitelist uid tid = do
tidStr <- asString tid
req <- baseRequest uid Galley Unversioned $ joinHttpPath ["i", "legalhold", "whitelisted-teams", tidStr]
submit "GET" req

setTeamFeatureConfig :: (HasCallStack, MakesValue domain, MakesValue team, MakesValue featureName, MakesValue payload) => Versioned -> domain -> team -> featureName -> payload -> App Response
setTeamFeatureConfig versioned domain team featureName payload = do
tid <- asString team
fn <- asString featureName
p <- make payload
req <- baseRequest domain Galley versioned $ joinHttpPath ["teams", tid, "features", fn]
submit "PUT" $ req & addJSON p
2 changes: 1 addition & 1 deletion integration/test/Test/Conversation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,7 @@ testDeleteTeamMemberLimitedEventFanout = do

-- Only the team admins will get the team-level event about Alex being removed
-- from the team
setTeamFeatureStatus OwnDomain team "limitedEventFanout" "enabled"
assertSuccess =<< setTeamFeatureStatus OwnDomain team "limitedEventFanout" "enabled"

withWebSockets [alice, amy, bob, alison, ana] $
\[wsAlice, wsAmy, wsBob, wsAlison, wsAna] -> do
Expand Down
86 changes: 84 additions & 2 deletions integration/test/Test/FeatureFlags.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,101 @@

module Test.FeatureFlags where

import qualified API.Galley as Public
import API.GalleyInternal
import qualified API.GalleyInternal as Internal
import Control.Monad.Reader
import qualified Data.Aeson as A
import SetupHelpers
import Testlib.Prelude

testLimitedEventFanout :: HasCallStack => App ()
testLimitedEventFanout = do
let featureName = "limitedEventFanout"
(_alice, team, _) <- createTeam OwnDomain 1
-- getTeamFeatureStatus OwnDomain team "limitedEventFanout" "enabled"
bindResponse (getTeamFeature OwnDomain featureName team) $ \resp -> do
resp.status `shouldMatchInt` 200
resp.json %. "status" `shouldMatch` "disabled"
setTeamFeatureStatus OwnDomain team featureName "enabled"
assertSuccess =<< setTeamFeatureStatus OwnDomain team featureName "enabled"
bindResponse (getTeamFeature OwnDomain featureName team) $ \resp -> do
resp.status `shouldMatchInt` 200
resp.json %. "status" `shouldMatch` "enabled"

disabled :: Value
disabled = object ["lockStatus" .= "unlocked", "status" .= "disabled", "ttl" .= "unlimited"]

disabledLocked :: Value
disabledLocked = object ["lockStatus" .= "locked", "status" .= "disabled", "ttl" .= "unlimited"]

enabled :: Value
enabled = object ["lockStatus" .= "unlocked", "status" .= "enabled", "ttl" .= "unlimited"]

checkFeature :: (HasCallStack, MakesValue user, MakesValue tid) => String -> user -> tid -> Value -> App ()
checkFeature feature user tid expected = do
tidStr <- asString tid
domain <- objDomain user
bindResponse (Internal.getTeamFeature domain tidStr feature) $ \resp -> do
resp.status `shouldMatchInt` 200
resp.json `shouldMatch` expected
bindResponse (Public.getTeamFeatures user tid) $ \resp -> do
resp.status `shouldMatchInt` 200
resp.json %. feature `shouldMatch` expected
bindResponse (Public.getTeamFeature user tid feature) $ \resp -> do
resp.status `shouldMatchInt` 200
resp.json `shouldMatch` expected
bindResponse (Public.getFeatureConfigs user) $ \resp -> do
resp.status `shouldMatchInt` 200
resp.json %. feature `shouldMatch` expected

testMlsE2EConfigCrlProxyRequired :: HasCallStack => App ()
testMlsE2EConfigCrlProxyRequired = do
(owner, tid, _) <- createTeam OwnDomain 1
let configWithoutCrlProxy =
object
[ "config"
.= object
[ "useProxyOnMobile" .= False,
"verificationExpiration" .= A.Number 86400
],
"status" .= "enabled"
]

-- From API version 6 onwards, the CRL proxy is required, so the request should fail when it's not provided
bindResponse (Internal.setTeamFeatureConfig Versioned owner tid "mlsE2EId" configWithoutCrlProxy) $ \resp -> do
resp.status `shouldMatchInt` 400
resp.json %. "label" `shouldMatch` "mls-e2eid-missing-crl-proxy"

configWithCrlProxy <-
configWithoutCrlProxy
& setField "config.useProxyOnMobile" True
& setField "config.crlProxy" "https://crl-proxy.example.com"
& setField "status" "enabled"

-- The request should succeed when the CRL proxy is provided
bindResponse (Internal.setTeamFeatureConfig Versioned owner tid "mlsE2EId" configWithCrlProxy) $ \resp -> do
resp.status `shouldMatchInt` 200

-- Assert that the feature config got updated correctly
expectedResponse <- configWithCrlProxy & setField "lockStatus" "unlocked" & setField "ttl" "unlimited"
checkFeature "mlsE2EId" owner tid expectedResponse

testMlsE2EConfigCrlProxyNotRequiredInV5 :: HasCallStack => App ()
testMlsE2EConfigCrlProxyNotRequiredInV5 = do
(owner, tid, _) <- createTeam OwnDomain 1
let configWithoutCrlProxy =
object
[ "config"
.= object
[ "useProxyOnMobile" .= False,
"verificationExpiration" .= A.Number 86400
],
"status" .= "enabled"
]

-- In API version 5, the CRL proxy is not required, so the request should succeed
bindResponse (Internal.setTeamFeatureConfig (ExplicitVersion 5) owner tid "mlsE2EId" configWithoutCrlProxy) $ \resp -> do
resp.status `shouldMatchInt` 200

-- Assert that the feature config got updated correctly
expectedResponse <- configWithoutCrlProxy & setField "lockStatus" "unlocked" & setField "ttl" "unlimited"
checkFeature "mlsE2EId" owner tid expectedResponse
4 changes: 2 additions & 2 deletions integration/test/Test/Search.hs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ federatedUserSearch d1 d2 test = do
u2 <- randomUser d2 def {BrigI.team = True}
uidD2 <- objId u2
team2 <- u2 %. "team"
GalleyI.setTeamFeatureStatus d2 team2 "searchVisibilityInbound" "enabled"
assertSuccess =<< GalleyI.setTeamFeatureStatus d2 team2 "searchVisibilityInbound" "enabled"

addTeamRestriction d1 d2 team2 test.restrictionD1D2
addTeamRestriction d2 d1 teamU1 test.restrictionD2D1
Expand Down Expand Up @@ -167,7 +167,7 @@ testFederatedUserSearchNonTeamSearcher = do
u1 <- randomUser d1 def
u2 <- randomUser d2 def {BrigI.team = True}
team2 <- u2 %. "team"
GalleyI.setTeamFeatureStatus d2 team2 "searchVisibilityInbound" "enabled"
assertSuccess =<< GalleyI.setTeamFeatureStatus d2 team2 "searchVisibilityInbound" "enabled"

u2Handle <- API.randomHandle
bindResponse (BrigP.putHandle u2 u2Handle) $ assertSuccess
Expand Down
4 changes: 2 additions & 2 deletions integration/test/Test/User.hs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ testUpdateHandle = do
bindResponse (getTeamFeature owner featureName team) $ \resp -> do
resp.status `shouldMatchInt` 200
resp.json %. "status" `shouldMatch` "disabled"
setTeamFeatureStatus owner team featureName "enabled"
assertSuccess =<< setTeamFeatureStatus owner team featureName "enabled"
bindResponse (getTeamFeature owner featureName team) $ \resp -> do
resp.status `shouldMatchInt` 200
resp.json %. "status" `shouldMatch` "enabled"
Expand Down Expand Up @@ -129,7 +129,7 @@ testUpdateSelf mode = do
bindResponse (getTeamFeature owner featureName team) $ \resp -> do
resp.status `shouldMatchInt` 200
resp.json %. "status" `shouldMatch` "disabled"
setTeamFeatureStatus owner team featureName "enabled"
assertSuccess =<< setTeamFeatureStatus owner team featureName "enabled"
bindResponse (getTeamFeature owner featureName team) $ \resp -> do
resp.status `shouldMatchInt` 200
resp.json %. "status" `shouldMatch` "enabled"
Expand Down
4 changes: 4 additions & 0 deletions integration/test/Testlib/HTTP.hs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ getJSON status resp = withResponse resp $ \r -> do
assertSuccess :: HasCallStack => Response -> App ()
assertSuccess resp = withResponse resp $ \r -> r.status `shouldMatchRange` (200, 299)

-- | assert a response status code
assertStatus :: HasCallStack => Int -> Response -> App ()
assertStatus status = flip withResponse \resp -> resp.status `shouldMatchInt` status

onFailureAddResponse :: HasCallStack => Response -> App a -> App a
onFailureAddResponse r m = App $ do
e <- ask
Expand Down
4 changes: 4 additions & 0 deletions libs/wire-api/src/Wire/API/Error/Galley.hs
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ data TeamFeatureError
| DisableSsoNotImplemented
| FeatureLocked
| MLSProtocolMismatch
| MLSE2EIDMissingCrlProxy

instance IsSwaggerError TeamFeatureError where
-- Do not display in Swagger
Expand Down Expand Up @@ -397,6 +398,8 @@ type instance MapError 'FeatureLocked = 'StaticError 409 "feature-locked" "Featu

type instance MapError 'MLSProtocolMismatch = 'StaticError 400 "mls-protocol-mismatch" "The default protocol needs to be part of the supported protocols"

type instance MapError 'MLSE2EIDMissingCrlProxy = 'StaticError 400 "mls-e2eid-missing-crl-proxy" "The field 'crlProxy' is missing in the request payload"

type instance ErrorEffect TeamFeatureError = Error TeamFeatureError

instance Member (Error DynError) r => ServerEffect (Error TeamFeatureError) r where
Expand All @@ -407,6 +410,7 @@ instance Member (Error DynError) r => ServerEffect (Error TeamFeatureError) r wh
DisableSsoNotImplemented -> dynError @(MapError 'DisableSsoNotImplemented)
FeatureLocked -> dynError @(MapError 'FeatureLocked)
MLSProtocolMismatch -> dynError @(MapError 'MLSProtocolMismatch)
MLSE2EIDMissingCrlProxy -> dynError @(MapError 'MLSE2EIDMissingCrlProxy)

--------------------------------------------------------------------------------
-- Proposal failure
Expand Down
2 changes: 2 additions & 0 deletions libs/wire-api/src/Wire/API/Routes/Named.hs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ namedClient = clientIn (Proxy @endpoint) (Proxy @m)

type family x ::> api

infixr 4 ::>

type instance
x ::> (Named name api) =
Named name (x :> api)
3 changes: 2 additions & 1 deletion libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ type FeatureAPI =
:<|> FeatureStatusGet OutlookCalIntegrationConfig
:<|> FeatureStatusPut '[] '() OutlookCalIntegrationConfig
:<|> From 'V5 ::> FeatureStatusGet MlsE2EIdConfig
:<|> From 'V5 ::> FeatureStatusPut '[] '() MlsE2EIdConfig
:<|> From 'V5 ::> Until 'V6 ::> Named "put-MlsE2EIdConfig@v5" (ZUser :> FeatureStatusBasePutPublic '() MlsE2EIdConfig)
:<|> From 'V6 ::> FeatureStatusPut '[] '() MlsE2EIdConfig
:<|> From 'V5 ::> FeatureStatusGet MlsMigrationConfig
:<|> From 'V5 ::> FeatureStatusPut '[] '() MlsMigrationConfig
:<|> From 'V5
Expand Down
10 changes: 8 additions & 2 deletions libs/wire-api/src/Wire/API/Team/Feature.hs
Original file line number Diff line number Diff line change
Expand Up @@ -1007,7 +1007,9 @@ instance FeatureTrivialConfig OutlookCalIntegrationConfig where

data MlsE2EIdConfig = MlsE2EIdConfig
{ verificationExpiration :: NominalDiffTime,
acmeDiscoveryUrl :: Maybe HttpsUrl
acmeDiscoveryUrl :: Maybe HttpsUrl,
crlProxy :: Maybe HttpsUrl,
useProxyOnMobile :: Bool
}
deriving stock (Eq, Show, Generic)

Expand All @@ -1019,6 +1021,8 @@ instance Arbitrary MlsE2EIdConfig where
MlsE2EIdConfig
<$> (fromIntegral <$> (arbitrary @Word32))
<*> arbitrary
<*> fmap Just arbitrary
<*> arbitrary

instance ToSchema MlsE2EIdConfig where
schema :: ValueSchema NamedSwaggerDoc MlsE2EIdConfig
Expand All @@ -1027,6 +1031,8 @@ instance ToSchema MlsE2EIdConfig where
MlsE2EIdConfig
<$> (toSeconds . verificationExpiration) .= fieldWithDocModifier "verificationExpiration" veDesc (fromSeconds <$> schema)
<*> acmeDiscoveryUrl .= maybe_ (optField "acmeDiscoveryUrl" schema)
<*> crlProxy .= maybe_ (optField "crlProxy" schema)
<*> useProxyOnMobile .= (fromMaybe False <$> optField "useProxyOnMobile" schema)
where
fromSeconds :: Int -> NominalDiffTime
fromSeconds = fromIntegral
Expand All @@ -1053,7 +1059,7 @@ instance IsFeatureConfig MlsE2EIdConfig where
type FeatureSymbol MlsE2EIdConfig = "mlsE2EId"
defFeatureStatus = withStatus FeatureStatusDisabled LockStatusUnlocked defValue FeatureTTLUnlimited
where
defValue = MlsE2EIdConfig (fromIntegral @Int (60 * 60 * 24)) Nothing
defValue = MlsE2EIdConfig (fromIntegral @Int (60 * 60 * 24)) Nothing Nothing False
featureSingleton = FeatureSingletonMlsE2EIdConfig
objectSchema = field "config" schema

Expand Down
3 changes: 2 additions & 1 deletion libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs
Original file line number Diff line number Diff line change
Expand Up @@ -1286,7 +1286,8 @@ tests =
],
testGroup "Golden: WithStatus_team 12" $
testObjects
[ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_18, "testObject_WithStatus_team_18.json")
[ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_18, "testObject_WithStatus_team_18.json"),
(Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_19, "testObject_WithStatus_team_19.json")
],
testGroup "Golden: InvitationRequest_team" $
testObjects [(Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_1, "testObject_InvitationRequest_team_1.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_2, "testObject_InvitationRequest_team_2.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_3, "testObject_InvitationRequest_team_3.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_4, "testObject_InvitationRequest_team_4.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_5, "testObject_InvitationRequest_team_5.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_6, "testObject_InvitationRequest_team_6.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_7, "testObject_InvitationRequest_team_7.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_8, "testObject_InvitationRequest_team_8.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_9, "testObject_InvitationRequest_team_9.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_10, "testObject_InvitationRequest_team_10.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_11, "testObject_InvitationRequest_team_11.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_12, "testObject_InvitationRequest_team_12.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_13, "testObject_InvitationRequest_team_13.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_14, "testObject_InvitationRequest_team_14.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_15, "testObject_InvitationRequest_team_15.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_16, "testObject_InvitationRequest_team_16.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_17, "testObject_InvitationRequest_team_17.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_18, "testObject_InvitationRequest_team_18.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_19, "testObject_InvitationRequest_team_19.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_20, "testObject_InvitationRequest_team_20.json")],
Expand Down
Loading

0 comments on commit 856d1f6

Please sign in to comment.