Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WPB-10772] Make it impossible for a user under legalhold to join an MLS conversation #4242

Merged
merged 8 commits into from
Sep 18, 2024
5 changes: 5 additions & 0 deletions changelog.d/2-features/WPB-10772
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
makes it impossible for a user to join an MLS conversation while already under legalhold (at least pending)

This implies two things:
- if a user is under legalhold they cannot ever join an MLS conversation, not even an MLS self conversation
- a user has to reject an to be put under legalhold when it wants to join MLS conversation (not explicitly acknowledging is not enough)
MangoIV marked this conversation as resolved.
Show resolved Hide resolved
70 changes: 70 additions & 0 deletions integration/test/Test/LegalHold.hs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import Data.ProtoLens.Labels ()
import qualified Data.Set as Set
import qualified Data.Text as T
import GHC.Stack
import MLS.Util
import Network.Wai (Request (pathInfo, requestMethod))
import Notifications
import Numeric.Lens (hex)
Expand Down Expand Up @@ -904,3 +905,72 @@ testLHDisableBeforeApproval = do
disableLegalHold tid alice bob defPassword
>>= assertStatus 200
getBob'sStatus `shouldMatch` "disabled"

-- ---------
-- WPB-10772
-- ---------

-- | scenario 2.1:
-- charlie first is put under legalhold and after that wants to join an MLS conversation
-- claiming a keypackage of charlie to add them to a conversation should not be possible
testLegalholdThenMLSThirdParty :: (HasCallStack) => App ()
testLegalholdThenMLSThirdParty = do
(alice, tid, [charlie]) <- createTeam OwnDomain 2
[alice1, charlie1] <- traverse (createMLSClient def) [alice, charlie]
_ <- uploadNewKeyPackage charlie1
_ <- createNewGroup alice1
legalholdWhitelistTeam tid alice >>= assertStatus 200
withMockServer def lhMockApp \lhDomAndPort _chan -> do
postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201
requestLegalHoldDevice tid alice charlie >>= assertSuccess
approveLegalHoldDevice tid (charlie %. "qualified_id") defPassword >>= assertSuccess
profile <- getUser alice charlie >>= getJSON 200
pStatus <- profile %. "legalhold_status" & asString
pStatus `shouldMatch` "enabled"

mls <- getMLSState
claimKeyPackages mls.ciphersuite alice1 charlie
`bindResponse` assertLabel 409 "mls-legal-hold-not-allowed"

-- | scenario 2.2:
-- charlie is put under legalhold but creates an MLS Group himself
-- since he doesn't need to claim his own keypackage to do so, this would succeed
-- we need to check upon group creation if the user is under legalhold and reject
-- the operation if they are
testLegalholdThenMLSSelf :: (HasCallStack) => App ()
testLegalholdThenMLSSelf = do
(alice, tid, [charlie]) <- createTeam OwnDomain 2
[alice1, charlie1] <- traverse (createMLSClient def) [alice, charlie]
_ <- uploadNewKeyPackage alice1
legalholdWhitelistTeam tid alice >>= assertStatus 200
withMockServer def lhMockApp \lhDomAndPort _chan -> do
postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201
requestLegalHoldDevice tid alice charlie >>= assertSuccess
approveLegalHoldDevice tid (charlie %. "qualified_id") defPassword >>= assertSuccess
profile <- getUser alice charlie >>= getJSON 200
pStatus <- profile %. "legalhold_status" & asString
pStatus `shouldMatch` "enabled"

-- charlie tries to create a group and should fail when POSTing the add commit
_ <- createNewGroup charlie1

void
-- we try to add alice since adding charlie himself would trigger 2.1
-- since he'd try to claim his own keypackages
$ createAddCommit charlie1 [alice]
>>= \mp ->
postMLSCommitBundle mp.sender (mkBundle mp)
`bindResponse` assertLabel 409 "mls-legal-hold-not-allowed"

-- (unsurprisingly) this same thing should also work in the one2one case

respJson <- getMLSOne2OneConversation alice charlie >>= getJSON 200
resetGroup alice1 (respJson %. "conversation")

void
-- we try to add alice since adding charlie himself would trigger 2.1
-- since he'd try to claim his own keypackages
$ createAddCommit charlie1 [alice]
>>= \mp ->
postMLSCommitBundle mp.sender (mkBundle mp)
`bindResponse` assertLabel 409 "mls-legal-hold-not-allowed"
12 changes: 12 additions & 0 deletions libs/types-common/src/Data/Qualified.hs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ module Data.Qualified
tSplit,
qTagUnsafe,
Remote,
pattern Remote,
pattern Local,
toRemoteUnsafe,
Local,
toLocalUnsafe,
Expand Down Expand Up @@ -127,6 +129,16 @@ foldQualified loc f g q
| otherwise =
g (qTagUnsafe q)

pattern Local :: forall a x. forall. Local a -> (Local x, Qualified a)
MangoIV marked this conversation as resolved.
Show resolved Hide resolved
pattern Local loc <- ((\(loc, q) -> (tDomain loc == qDomain q, qUnqualified q <$ loc)) -> (True, loc))

pattern Remote :: forall a x. forall. Remote a -> (Local x, Qualified a)
pattern Remote loc <- ((\(loc, q) -> (tDomain loc == qDomain q, qTagUnsafe q)) -> (False, loc))

{-# COMPLETE Local, Remote #-}

-- pattern Remote :: Remote a -> (Local x, Qualified a)
MangoIV marked this conversation as resolved.
Show resolved Hide resolved

-- Partition a collection of qualified values into locals and remotes.
--
-- Note that the local values are returned as unqualified values, as a (probably
Expand Down
3 changes: 2 additions & 1 deletion libs/wire-api-federation/src/Wire/API/Federation/API.hs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ module Wire.API.Federation.API
fedQueueClient,
sendBundle,
fedClientIn,
module Wire.API.MakesFederatedCall,
module X,

-- * Re-exports
Component (..),
Expand Down Expand Up @@ -59,6 +59,7 @@ import Wire.API.Federation.Endpoint
import Wire.API.Federation.HasNotificationEndpoint
import Wire.API.Federation.Version
import Wire.API.MakesFederatedCall
import Wire.API.MakesFederatedCall as X hiding (Location (..))
import Wire.API.Routes.Named

-- Note: this type family being injective means that in most cases there is no need
Expand Down
5 changes: 5 additions & 0 deletions libs/wire-api/src/Wire/API/Error.hs
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@ module Wire.API.Error
throwS,
noteS,
mapErrorS,
runErrorS,
mapToRuntimeError,
mapToDynamicError,
)
where

import Control.Error (hush)
import Control.Lens (at, (%~), (.~), (?~))
import Data.Aeson (FromJSON (..), ToJSON (..))
import Data.Aeson qualified as A
Expand Down Expand Up @@ -272,6 +274,9 @@ throwS = throw (Tagged @e ())
noteS :: forall e r a. (Member (ErrorS e) r) => Maybe a -> Sem r a
noteS = note (Tagged @e ())

runErrorS :: forall e r a. Sem (ErrorS e : r) a -> Sem r (Maybe a)
runErrorS = fmap hush . runError @(Tagged e ())

mapErrorS ::
forall e e' r a.
(Member (ErrorS e') r) =>
Expand Down
5 changes: 5 additions & 0 deletions libs/wire-api/src/Wire/API/Error/Galley.hs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ data GalleyError
| MLSSubConvClientNotInParent
| MLSMigrationCriteriaNotSatisfied
| MLSFederatedOne2OneNotSupported
| -- | MLS and federation are incompatible with legalhold - this error is thrown if a user
-- tries to create an MLS group while being under legalhold
MLSLegalholdIncompatible
| --
NoBindingTeamMembers
| NoBindingTeam
Expand Down Expand Up @@ -256,6 +259,8 @@ type instance MapError 'MLSMigrationCriteriaNotSatisfied = 'StaticError 400 "mls

type instance MapError 'MLSFederatedOne2OneNotSupported = 'StaticError 400 "mls-federated-one2one-not-supported" "Federated One2One MLS conversations are only supported in API version >= 6"

type instance MapError MLSLegalholdIncompatible = 'StaticError 409 "mls-legal-hold-not-allowed" "A user who is under legal-hold may not participate in MLS conversations"

type instance MapError 'NoBindingTeamMembers = 'StaticError 403 "non-binding-team-members" "Both users must be members of the same binding team"

type instance MapError 'NoBindingTeam = 'StaticError 403 "no-binding-team" "Operation allowed only on binding teams"
Expand Down
57 changes: 29 additions & 28 deletions libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs
Original file line number Diff line number Diff line change
Expand Up @@ -72,33 +72,34 @@ type MLSMessagingAPI =
:<|> Named
"mls-commit-bundle"
( Summary "Post a MLS CommitBundle"
:> From 'V5
:> MakesFederatedCall 'Galley "on-mls-message-sent"
:> MakesFederatedCall 'Galley "mls-welcome"
:> MakesFederatedCall 'Galley "send-mls-commit-bundle"
:> MakesFederatedCall 'Galley "on-conversation-updated"
:> MakesFederatedCall 'Brig "get-mls-clients"
:> MakesFederatedCall 'Brig "get-users-by-ids"
:> MakesFederatedCall 'Brig "api-version"
:> CanThrow 'ConvAccessDenied
:> CanThrow 'ConvMemberNotFound
:> CanThrow 'ConvNotFound
:> CanThrow 'LegalHoldNotEnabled
:> CanThrow 'MissingLegalholdConsent
:> CanThrow 'MLSClientMismatch
:> CanThrow 'MLSClientSenderUserMismatch
:> CanThrow 'MLSCommitMissingReferences
:> CanThrow 'MLSGroupConversationMismatch
:> CanThrow 'MLSInvalidLeafNodeIndex
:> CanThrow 'MLSNotEnabled
:> CanThrow 'MLSProposalNotFound
:> CanThrow 'MLSProtocolErrorTag
:> CanThrow 'MLSSelfRemovalNotAllowed
:> CanThrow 'MLSStaleMessage
:> CanThrow 'MLSSubConvClientNotInParent
:> CanThrow 'MLSUnsupportedMessage
:> CanThrow 'MLSUnsupportedProposal
:> CanThrow 'MLSWelcomeMismatch
:> From V5
:> MakesFederatedCall Galley "on-mls-message-sent"
:> MakesFederatedCall Galley "mls-welcome"
:> MakesFederatedCall Galley "send-mls-commit-bundle"
:> MakesFederatedCall Galley "on-conversation-updated"
:> MakesFederatedCall Brig "get-mls-clients"
:> MakesFederatedCall Brig "get-users-by-ids"
:> MakesFederatedCall Brig "api-version"
:> CanThrow ConvAccessDenied
:> CanThrow ConvMemberNotFound
:> CanThrow ConvNotFound
:> CanThrow LegalHoldNotEnabled
:> CanThrow MissingLegalholdConsent
:> CanThrow MLSClientMismatch
:> CanThrow MLSClientSenderUserMismatch
:> CanThrow MLSCommitMissingReferences
:> CanThrow MLSGroupConversationMismatch
:> CanThrow MLSInvalidLeafNodeIndex
:> CanThrow MLSNotEnabled
:> CanThrow MLSProposalNotFound
:> CanThrow MLSProtocolErrorTag
:> CanThrow MLSSelfRemovalNotAllowed
:> CanThrow MLSStaleMessage
:> CanThrow MLSSubConvClientNotInParent
:> CanThrow MLSUnsupportedMessage
:> CanThrow MLSUnsupportedProposal
:> CanThrow MLSWelcomeMismatch
:> CanThrow MLSLegalholdIncompatible
:> CanThrow MLSProposalFailure
:> CanThrow NonFederatingBackends
:> CanThrow UnreachableBackends
Expand All @@ -107,7 +108,7 @@ type MLSMessagingAPI =
:> ZClient
:> ZConn
:> ReqBody '[MLS] (RawMLS CommitBundle)
:> MultiVerb1 'POST '[JSON] (Respond 201 "Commit accepted and forwarded" MLSMessageSendingStatus)
:> MultiVerb1 POST '[JSON] (Respond 201 "Commit accepted and forwarded" MLSMessageSendingStatus)
)
:<|> Named
"mls-public-keys"
Expand Down
3 changes: 3 additions & 0 deletions libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team
import Wire.API.Team
import Wire.API.Team.Conversation qualified as Conv
import Wire.API.Team.Feature
import Wire.API.Team.LegalHold
import Wire.API.Team.Member qualified as Team
import Wire.API.Team.Role
import Wire.API.Team.SearchVisibility
Expand Down Expand Up @@ -94,6 +95,8 @@ data GalleyAPIAccess m a where
GetTeamLegalHoldStatus ::
TeamId ->
GalleyAPIAccess m (LockableFeature LegalholdConfig)
GetUserLegalholdStatus ::
Local UserId -> TeamId -> GalleyAPIAccess m UserLegalHoldStatusResponse
GetTeamSearchVisibility ::
TeamId ->
GalleyAPIAccess m TeamSearchVisibility
Expand Down
20 changes: 20 additions & 0 deletions libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import Wire.API.Routes.Version
import Wire.API.Team
import Wire.API.Team.Conversation qualified as Conv
import Wire.API.Team.Feature
import Wire.API.Team.LegalHold
import Wire.API.Team.Member as Member
import Wire.API.Team.Role
import Wire.API.Team.SearchVisibility
Expand Down Expand Up @@ -80,6 +81,7 @@ interpretGalleyAPIAccessToRpc disabledVersions galleyEndpoint =
GetTeamName id' -> getTeamName id'
GetTeamLegalHoldStatus id' -> getTeamLegalHoldStatus id'
GetTeamSearchVisibility id' -> getTeamSearchVisibility id'
GetUserLegalholdStatus id' tid -> getUserLegalholdStatus id' tid
ChangeTeamStatus id' ts m_al -> changeTeamStatus id' ts m_al
MemberIsTeamOwner id' id'' -> memberIsTeamOwner id' id''
GetAllTeamFeaturesForUser m_id' -> getAllTeamFeaturesForUser m_id'
Expand All @@ -89,6 +91,24 @@ interpretGalleyAPIAccessToRpc disabledVersions galleyEndpoint =
UnblockConversation lusr mconn qcnv -> unblockConversation v lusr mconn qcnv
GetEJPDConvInfo uid -> getEJPDConvInfo uid

getUserLegalholdStatus ::
( Member TinyLog r,
Member (Error ParseException) r,
Member Rpc r
) =>
Local UserId ->
TeamId ->
Sem (Input Endpoint : r) UserLegalHoldStatusResponse
getUserLegalholdStatus luid tid = do
debug $
remote "galley"
. msg (val "get legalhold user status")
decodeBodyOrThrow "galley" =<< galleyRequest do
method GET
. paths ["teams", toByteString' tid, "legalhold", toByteString' (tUnqualified luid)]
. zUser (tUnqualified luid)
. expect2xx

galleyRequest :: (Member Rpc r, Member (Input Endpoint) r) => (Request -> Request) -> Sem r (Response (Maybe LByteString))
galleyRequest req = do
ep <- input
Expand Down
1 change: 1 addition & 0 deletions services/brig/src/Brig/API/Error.hs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ clientError (ClientDataError e) = clientDataError e
clientError (ClientUserNotFound _) = StdError (errorToWai @'E.InvalidUser)
clientError ClientLegalHoldCannotBeRemoved = StdError can'tDeleteLegalHoldClient
clientError ClientLegalHoldCannotBeAdded = StdError can'tAddLegalHoldClient
clientError ClientLegalHoldIncompatible = StdError $ Wai.mkError status409 "mls-legal-hold-not-allowed" "A user who is under legal-hold may not participate in MLS conversations"
clientError (ClientFederationError e) = fedError e
clientError ClientCapabilitiesCannotBeRemoved = StdError clientCapabilitiesCannotBeRemoved
clientError ClientMissingLegalholdConsentOldClients = StdError (errorToWai @'E.MissingLegalholdConsentOldClients)
Expand Down
4 changes: 3 additions & 1 deletion services/brig/src/Brig/API/Federation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import Gundeck.Types.Push qualified as Push
import Imports hiding ((\\))
import Network.Wai.Utilities.Error ((!>>))
import Polysemy
import Polysemy.Fail (Fail)
import Servant (ServerT)
import Servant.API
import Wire.API.Connection
Expand Down Expand Up @@ -87,6 +88,7 @@ federationSitemap ::
Member NotificationSubsystem r,
Member UserSubsystem r,
Member UserStore r,
Member Fail r,
Member DeleteQueue r
) =>
ServerT FederationAPI (Handler r)
Expand Down Expand Up @@ -193,7 +195,7 @@ claimMultiPrekeyBundle ::
Handler r UserClientPrekeyMap
claimMultiPrekeyBundle _ uc = API.claimLocalMultiPrekeyBundles LegalholdPlusFederationNotImplemented uc !>> clientError

fedClaimKeyPackages :: Domain -> ClaimKeyPackageRequest -> Handler r (Maybe KeyPackageBundle)
fedClaimKeyPackages :: (Member Fail r, Member GalleyAPIAccess r, Member UserStore r) => Domain -> ClaimKeyPackageRequest -> Handler r (Maybe KeyPackageBundle)
fedClaimKeyPackages domain ckpr =
isMLSEnabled >>= \case
True -> do
Expand Down
Loading
Loading