Skip to content

Commit

Permalink
Initial MLS configuration for new teams (#4262)
Browse files Browse the repository at this point in the history
* Add initialConfig to mls flag configuration

* Simplify createBindingTeam

* Initialise MLS feature flag for new teams

* Test mls flag initialisation

* Document initialConfig for mls feature flag

* Test mls initial configuration when locked

* Add CHANGELOG entry

* Regenerate nix packages
  • Loading branch information
pcapriotti authored Oct 1, 2024
1 parent 5ff7446 commit 433cc0d
Show file tree
Hide file tree
Showing 12 changed files with 152 additions and 32 deletions.
1 change: 1 addition & 0 deletions changelog.d/2-features/new-teams-mls
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `initialConfig` setting for the `mls` feature flag
13 changes: 13 additions & 0 deletions docs/src/developer/reference/config-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,19 @@ mls:

This default configuration can be overriden on a per-team basis through the [feature config API](../developer/features.md)

This flag also supports setting an `initialConfig` value, which is applied when a team is created:

```yaml
# galley.yaml
mls:
initialConfig:
protocolToggleUsers: []
defaultProtocol: mls
supportedProtocols: [proteus, mls] # must contain defaultProtocol
allowedCipherSuites: [1]
defaultCipherSuite: 1
```

### MLS End-to-End Identity

The MLS end-to-end identity team feature adds an extra level of security and practicability. If turned on, automatic device authentication ensures that team members know they are communicating with people using authenticated devices. Team members get a certificate on all their devices.
Expand Down
1 change: 1 addition & 0 deletions integration/integration.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ library
Test.FeatureFlags.EnforceFileDownloadLocation
Test.FeatureFlags.FileSharing
Test.FeatureFlags.GuestLinks
Test.FeatureFlags.Initialisation
Test.FeatureFlags.LegalHold
Test.FeatureFlags.Mls
Test.FeatureFlags.MlsE2EId
Expand Down
3 changes: 1 addition & 2 deletions integration/test/SetupHelpers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ deleteUser user = bindResponse (API.Brig.deleteUser user) $ \resp -> do
-- | returns (owner, team id, members)
createTeam :: (HasCallStack, MakesValue domain) => domain -> Int -> App (Value, String, [Value])
createTeam domain memberCount = do
res <- createUser domain def {team = True}
owner <- res.json
owner <- createUser domain def {team = True} >>= getJSON 201
tid <- owner %. "team" & asString
members <- for [2 .. memberCount] $ \_ -> createTeamMember owner tid
pure (owner, tid, members)
Expand Down
68 changes: 68 additions & 0 deletions integration/test/Test/FeatureFlags/Initialisation.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
module Test.FeatureFlags.Initialisation where

import API.GalleyInternal
import Control.Monad.Codensity
import Control.Monad.Extra
import Control.Monad.Reader
import SetupHelpers
import Testlib.Prelude
import Testlib.ResourcePool

testMLSInitialisation :: (HasCallStack) => App ()
testMLSInitialisation = do
let override =
def
{ galleyCfg =
setField
"settings.featureFlags.mls"
( object
[ "initialConfig"
.= object
[ "protocolToggleUsers" .= ([] :: [Int]),
"defaultProtocol" .= "mls",
"allowedCipherSuites" .= [1, 2 :: Int],
"defaultCipherSuite" .= (1 :: Int),
"supportedProtocols" .= ["mls", "proteus"]
]
]
)
>=> removeField "settings.featureFlags.mlsMigration"
}

pool <- asks (.resourcePool)
lowerCodensity do
[resource] <- acquireResources 1 pool

(alice, aliceTeam) <- lift $ lowerCodensity do
-- start a dynamic backend with default configuration
domain <- startDynamicBackend resource def

-- create a team
lift do
(alice, tid, _) <- createTeam domain 0
feat <- getTeamFeature alice tid "mls" >>= getJSON 200
feat %. "config.defaultProtocol" `shouldMatch` "proteus"
pure (alice, tid)

lift $ lowerCodensity do
-- now start the backend again, this time with an initial mls
-- configuration set
domain <- startDynamicBackend resource override

-- a pre-existing team should get the default configuration
lift do
feat <- getTeamFeature alice aliceTeam "mls" >>= getJSON 200
feat %. "config.defaultProtocol" `shouldMatch` "proteus"

-- a new team should get the initial mls configuration
lift do
(bob, tid, _) <- createTeam domain 0
feat <- getTeamFeature bob tid "mls" >>= getJSON 200
feat %. "config.defaultProtocol" `shouldMatch` "mls"

-- if the mls feature is locked, the config reverts back to default
void
$ patchTeamFeature bob tid "mls" (object ["lockStatus" .= "locked"])
>>= getJSON 200
feat' <- getTeamFeature bob tid "mls" >>= getJSON 200
feat' %. "config.defaultProtocol" `shouldMatch` "proteus"
8 changes: 6 additions & 2 deletions integration/test/Testlib/ModService.hs
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,14 @@ startDynamicBackends beOverrides k =
when (Prelude.length beOverrides > 3) $ lift $ failApp "Too many backends. Currently only 3 are supported."
pool <- asks (.resourcePool)
resources <- acquireResources (Prelude.length beOverrides) pool
void $ traverseConcurrentlyCodensity (uncurry startDynamicBackend) (zip resources beOverrides)
void $
traverseConcurrentlyCodensity
(void . uncurry startDynamicBackend)
(zip resources beOverrides)
pure $ map (.berDomain) resources
k

startDynamicBackend :: BackendResource -> ServiceOverrides -> Codensity App ()
startDynamicBackend :: BackendResource -> ServiceOverrides -> Codensity App String
startDynamicBackend resource beOverrides = do
let overrides =
mconcat
Expand All @@ -141,6 +144,7 @@ startDynamicBackend resource beOverrides = do
beOverrides
]
startBackend resource overrides
pure resource.berDomain
where
setAwsConfigs :: ServiceOverrides
setAwsConfigs =
Expand Down
2 changes: 1 addition & 1 deletion integration/test/Testlib/RunServices.hs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@ main = do
$ do
_modifyEnv <-
traverseConcurrentlyCodensity
(\r -> startDynamicBackend r mempty)
(\r -> void $ startDynamicBackend r mempty)
[backendA, backendB]
liftIO run
2 changes: 2 additions & 0 deletions libs/galley-types/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
, lens
, lib
, memory
, schema-profunctor
, sop-core
, text
, types-common
Expand All @@ -39,6 +40,7 @@ mkDerivation {
imports
lens
memory
schema-profunctor
sop-core
text
types-common
Expand Down
1 change: 1 addition & 0 deletions libs/galley-types/galley-types.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ library
, imports
, lens >=4.12
, memory
, schema-profunctor
, sop-core
, text >=0.11
, types-common >=0.16
Expand Down
32 changes: 30 additions & 2 deletions libs/galley-types/src/Galley/Types/Teams.hs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ module Galley.Types.Teams
GetFeatureDefaults (..),
FeatureDefaults (..),
FeatureFlags,
DefaultsInitial (..),
initialFeature,
featureDefaults,
notTeamMember,
findTeamMember,
Expand All @@ -47,6 +49,7 @@ import Data.ByteString.UTF8 qualified as UTF8
import Data.Default
import Data.Id (UserId)
import Data.SOP
import Data.Schema qualified as S
import Data.Set qualified as Set
import Imports
import Wire.API.Team.Feature
Expand Down Expand Up @@ -214,10 +217,10 @@ newtype instance FeatureDefaults SndFactorPasswordChallengeConfig
deriving (FromJSON) via Defaults (LockableFeature SndFactorPasswordChallengeConfig)
deriving (ParseFeatureDefaults) via OptionalField SndFactorPasswordChallengeConfig

newtype instance FeatureDefaults MLSConfig = MLSDefaults (LockableFeature MLSConfig)
newtype instance FeatureDefaults MLSConfig = MLSDefaults (DefaultsInitial MLSConfig)
deriving stock (Eq, Show)
deriving newtype (Default, GetFeatureDefaults)
deriving (FromJSON) via Defaults (LockableFeature MLSConfig)
deriving (FromJSON) via DefaultsInitial MLSConfig
deriving (ParseFeatureDefaults) via OptionalField MLSConfig

data instance FeatureDefaults ExposeInvitationURLsToTeamAdminConfig
Expand Down Expand Up @@ -328,6 +331,31 @@ instance (FromJSON a) => FromJSON (Defaults a) where
parseJSON = withObject "default object" $ \ob ->
Defaults <$> (ob .: "defaults")

data DefaultsInitial cfg = DefaultsInitial
{ defFeature :: LockableFeature cfg,
initial :: cfg
}
deriving (Eq, Show)

instance (IsFeatureConfig cfg) => Default (DefaultsInitial cfg) where
def = DefaultsInitial def def

type instance ConfigOf (DefaultsInitial cfg) = cfg

instance GetFeatureDefaults (DefaultsInitial cfg) where
featureDefaults1 = defFeature

instance (IsFeatureConfig cfg) => FromJSON (DefaultsInitial cfg) where
parseJSON = withObject "default with initial" $ \ob -> do
feat <- ob .:? "defaults" .!= def
mc <-
fromMaybe feat.config
<$> A.explicitParseFieldMaybe S.schemaParseJSON ob "initialConfig"
pure $ DefaultsInitial feat mc

initialFeature :: DefaultsInitial cfg -> LockableFeature cfg
initialFeature d = d.defFeature {config = d.initial}

makeLenses ''TeamCreationTime

notTeamMember :: [UserId] -> [TeamMember] -> [UserId]
Expand Down
33 changes: 10 additions & 23 deletions services/galley/src/Galley/API/Teams.hs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import Data.Time.Clock (UTCTime)
import Galley.API.Action
import Galley.API.Error as Galley
import Galley.API.LegalHold.Team
import Galley.API.Teams.Features
import Galley.API.Teams.Features.Get
import Galley.API.Teams.Notifications qualified as APITeamQueue
import Galley.API.Update qualified as API
Expand Down Expand Up @@ -239,6 +240,8 @@ createNonBindingTeamH _ _ _ = do
createBindingTeam ::
( Member NotificationSubsystem r,
Member (Input UTCTime) r,
Member (Input Opts) r,
Member TeamFeatureStore r,
Member TeamStore r
) =>
TeamId ->
Expand All @@ -249,7 +252,13 @@ createBindingTeam tid zusr body = do
let owner = Public.mkTeamMember zusr fullPermissions Nothing LH.defUserLegalHoldStatus
team <-
E.createTeam (Just tid) zusr body.newTeamName body.newTeamIcon body.newTeamIconKey Binding
finishCreateTeam team owner [] Nothing
initialiseTeamFeatures tid

E.createTeamMember tid owner
now <- input
let e = newEvent tid now (EdTeamCreate team)
pushNotifications
[newPushLocal1 zusr (toJSONObject e) (userRecipient zusr :| [])]
pure tid

updateTeamStatus ::
Expand Down Expand Up @@ -1313,28 +1322,6 @@ addTeamMemberInternal tid origin originConn (ntmNewTeamMember -> new) = do
APITeamQueue.pushTeamEvent tid e
pure sizeBeforeAdd

finishCreateTeam ::
( Member NotificationSubsystem r,
Member (Input UTCTime) r,
Member TeamStore r
) =>
Team ->
TeamMember ->
[TeamMember] ->
Maybe ConnId ->
Sem r ()
finishCreateTeam team owner others zcon = do
let zusr = owner ^. userId
for_ (owner : others) $
E.createTeamMember (team ^. teamId)
now <- input
let e = newEvent (team ^. teamId) now (EdTeamCreate team)
let r = membersToRecipients Nothing others
pushNotifications
[ newPushLocal1 zusr (toJSONObject e) (userRecipient zusr :| r)
& pushConn .~ zcon
]

getBindingTeamMembers ::
( Member (ErrorS 'TeamNotFound) r,
Member (ErrorS 'NonBindingTeam) r,
Expand Down
20 changes: 18 additions & 2 deletions services/galley/src/Galley/API/Teams/Features.hs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module Galley.API.Teams.Features
guardSecondFactorDisabled,
featureEnabledForTeam,
guardMlsE2EIdConfig,
initialiseTeamFeatures,
)
where

Expand All @@ -43,7 +44,7 @@ import Data.Qualified (Local)
import Data.Time (UTCTime)
import Galley.API.Error (InternalError)
import Galley.API.LegalHold qualified as LegalHold
import Galley.API.Teams (ensureNotTooLargeToActivateLegalHold)
import Galley.API.LegalHold.Team qualified as LegalHold
import Galley.API.Teams.Features.Get
import Galley.API.Util (assertTeamExists, getTeamMembersForFanout, membersToRecipients, permissionCheck)
import Galley.App
Expand Down Expand Up @@ -243,6 +244,21 @@ setFeatureForTeam tid feat = do
pushFeatureEvent tid (mkUpdateEvent newFeat)
pure newFeat

initialiseTeamFeatures ::
( Member (Input Opts) r,
Member TeamFeatureStore r
) =>
TeamId ->
Sem r ()
initialiseTeamFeatures tid = do
flags :: FeatureFlags <- inputs $ view (settings . featureFlags)

-- set MLS initial config
let MLSDefaults fdef = npProject flags
let feat = initialFeature fdef
setDbFeature tid feat
pure ()

-------------------------------------------------------------------------------
-- SetFeatureConfig instances

Expand Down Expand Up @@ -349,7 +365,7 @@ instance SetFeatureConfig LegalholdConfig where

case feat.status of
FeatureStatusDisabled -> LegalHold.removeSettings' @InternalPaging tid
FeatureStatusEnabled -> ensureNotTooLargeToActivateLegalHold tid
FeatureStatusEnabled -> LegalHold.ensureNotTooLargeToActivateLegalHold tid
pure feat

instance SetFeatureConfig FileSharingConfig
Expand Down

0 comments on commit 433cc0d

Please sign in to comment.