Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b95a61d
WPB-18190: Add route to delete collaborator from team
blackheaven Jul 30, 2025
bcbb3ac
add conversation drop
blackheaven Aug 1, 2025
d5bfa92
debug
blackheaven Aug 4, 2025
e01e238
go through GalleyAPI
blackheaven Aug 4, 2025
3ee10e1
filter then delete
blackheaven Aug 5, 2025
a5e3ca0
missing dependency
blackheaven Aug 5, 2025
c829a54
fix missing nix dependency
blackheaven Aug 5, 2025
0056512
fix: remove O2O Conversations and remove users from others
blackheaven Aug 6, 2025
371cb23
refactor: split team quitting and user deletion
blackheaven Aug 14, 2025
da893a2
rely on Galley
blackheaven Aug 14, 2025
905a436
fix: restore team collaborator removal
blackheaven Aug 15, 2025
72998e3
Update changelog.d/2-features/WPB-18190
blackheaven Aug 19, 2025
fe5fe64
test: add multiple team & check get conv
blackheaven Aug 19, 2025
0581297
fix: other connection test
blackheaven Aug 20, 2025
1c140e1
test: add group conversation
blackheaven Aug 20, 2025
f756df8
fix: drop Cassandra IN search
blackheaven Aug 22, 2025
ebecae2
refactor: drop explicit queries
blackheaven Aug 27, 2025
6d5b256
feat: filter-out O2O connections
blackheaven Aug 28, 2025
3160115
fix: rebase
blackheaven Aug 28, 2025
67ae560
fix: missing parts
blackheaven Aug 28, 2025
f01db28
WPB-18191: Add route to collaborator permissions from team
blackheaven Jul 31, 2025
1e059e2
trigger conversation closing
blackheaven Aug 4, 2025
6164bb2
fix formating
blackheaven Aug 5, 2025
aa0dcda
fix: drop redundant constraint
blackheaven Aug 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/2-features/WPB-18190
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow collaborator to be removed from a team.
1 change: 1 addition & 0 deletions changelog.d/2-features/WPB-18191
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow member permissions to be updated in a team.
14 changes: 14 additions & 0 deletions integration/test/API/Brig.hs
Original file line number Diff line number Diff line change
Expand Up @@ -1133,3 +1133,17 @@ getAllTeamCollaborators :: (MakesValue owner) => owner -> String -> App Response
getAllTeamCollaborators owner tid = do
req <- baseRequest owner Brig Versioned $ joinHttpPath ["teams", tid, "collaborators"]
submit "GET" req

updateTeamCollaborator :: (MakesValue owner, MakesValue collaborator, HasCallStack) => owner -> String -> collaborator -> [String] -> App Response
updateTeamCollaborator owner tid collaborator permissions = do
(_, collabId) <- objQid collaborator
req <- baseRequest owner Brig Versioned $ joinHttpPath ["teams", tid, "collaborators", collabId]
submit "PUT" $
req
& addJSON permissions

removeTeamCollaborator :: (MakesValue owner, MakesValue collaborator, HasCallStack) => owner -> String -> collaborator -> App Response
removeTeamCollaborator owner tid collaborator = do
(_, collabId) <- objQid collaborator
req <- baseRequest owner Brig Versioned $ joinHttpPath ["teams", tid, "collaborators", collabId]
submit "DELETE" req
84 changes: 83 additions & 1 deletion integration/test/Test/TeamCollaborators.hs
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,86 @@ testImplicitConnectionNoCollaborator = do
-- Alice and Bob aren't connected at all.
postOne2OneConversation bob alice team0 "chit-chat" >>= assertLabel 403 "no-team-member"

postOne2OneConversation alice bob team0 "chat-chit" >>= assertLabel 403 "non-binding-team-members"
testRemoveMemberInO2O :: (HasCallStack) => App ()
testRemoveMemberInO2O = do
(owner0, team0, [alice]) <- createTeam OwnDomain 2
(owner1, team1, [bob]) <- createTeam OwnDomain 2

-- At the time of writing, it wasn't clear if this should be a bot instead.
charlie <- randomUser OwnDomain def
addTeamCollaborator owner0 team0 charlie ["implicit_connection"] >>= assertSuccess
addTeamCollaborator owner1 team1 charlie ["implicit_connection"] >>= assertSuccess

postOne2OneConversation charlie alice team0 "chit-chat" >>= assertSuccess
postOne2OneConversation charlie bob team1 "chit-chat" >>= assertSuccess

removeTeamCollaborator owner0 team0 charlie >>= assertSuccess

getMLSOne2OneConversation charlie alice >>= assertLabel 403 "not-connected"
postOne2OneConversation charlie alice team0 "chit-chat" >>= assertLabel 403 "no-team-member"
getMLSOne2OneConversation charlie bob >>= assertSuccess

testRemoveMemberInO2OConnected :: (HasCallStack) => App ()
testRemoveMemberInO2OConnected = do
(owner0, team0, [alice]) <- createTeam OwnDomain 2

-- At the time of writing, it wasn't clear if this should be a bot instead.
bob <- randomUser OwnDomain def
addTeamCollaborator owner0 team0 bob ["implicit_connection"] >>= assertSuccess

postOne2OneConversation bob alice team0 "chit-chat" >>= assertSuccess
connectTwoUsers alice bob

removeTeamCollaborator owner0 team0 bob >>= assertSuccess

getMLSOne2OneConversation bob alice >>= assertSuccess

testRemoveMemberInTeamConversation :: (HasCallStack) => App ()
testRemoveMemberInTeamConversation = do
(owner, team, [alice, bob]) <- createTeam OwnDomain 3

aliceId <- alice %. "qualified_id"
bobId <- bob %. "qualified_id"
conv <-
postConversation
owner
defProteus {team = Just team, skipCreator = Just True, qualifiedUsers = [aliceId, bobId]}
>>= getJSON 201

removeTeamCollaborator owner team bob >>= assertSuccess

getConversation alice conv `bindResponse` \resp -> do
resp.status `shouldMatchInt` 200

getConversation bob conv `bindResponse` \resp -> do
resp.status `shouldMatchInt` 403

testUpdateMember :: (HasCallStack) => App ()
testUpdateMember = do
(owner, team, [alice]) <- createTeam OwnDomain 2

-- At the time of writing, it wasn't clear if this should be a bot instead.
bob <- randomUser OwnDomain def
addTeamCollaborator
owner
team
bob
["implicit_connection"]
>>= assertSuccess
postOne2OneConversation bob alice team "chit-chat" >>= assertSuccess

updateTeamCollaborator
owner
team
bob
["create_team_conversation", "implicit_connection"]
>>= assertSuccess
postOne2OneConversation bob alice team "chit-chat" >>= assertSuccess

updateTeamCollaborator
owner
team
bob
[]
>>= assertSuccess
postOne2OneConversation bob alice team "chit-chat" >>= assertLabel 403 "operation-denied"
26 changes: 25 additions & 1 deletion libs/wire-api/src/Wire/API/Event/Team.hs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ data EventType
| ConvCreate
| ConvDelete
| CollaboratorAdd
| CollaboratorUpdate
| CollaboratorRemove
deriving stock (Eq, Show, Generic)
deriving (Arbitrary) via (GenericUniform EventType)
deriving (FromJSON, ToJSON, S.ToSchema) via Schema EventType
Expand All @@ -140,7 +142,9 @@ instance ToSchema EventType where
element "team.member-update" MemberUpdate,
element "team.conversation-create" ConvCreate,
element "team.conversation-delete" ConvDelete,
element "team.collaborator-add" CollaboratorAdd
element "team.collaborator-add" CollaboratorAdd,
element "team.collaborator-update" CollaboratorUpdate,
element "team.collaborator-remove" CollaboratorRemove
]

--------------------------------------------------------------------------------
Expand All @@ -156,6 +160,8 @@ data EventData
| EdConvCreate ConvId
| EdConvDelete ConvId
| EdCollaboratorAdd UserId [CollaboratorPermission]
| EdCollaboratorUpdate UserId [CollaboratorPermission]
| EdCollaboratorRemove UserId
deriving stock (Eq, Show, Generic)

-- FUTUREWORK: this is outright wrong; see "Wire.API.Event.Conversation" on how to do this properly.
Expand Down Expand Up @@ -185,6 +191,12 @@ instance ToJSON EventData where
[ "user" A..= usr,
"permissions" A..= perms
]
toJSON (EdCollaboratorUpdate usr perms) =
A.object
[ "user" A..= usr,
"permissions" A..= perms
]
toJSON (EdCollaboratorRemove usr) = A.object ["user" A..= usr]

eventDataType :: EventData -> EventType
eventDataType (EdTeamCreate _) = TeamCreate
Expand All @@ -196,6 +208,8 @@ eventDataType (EdMemberUpdate _ _) = MemberUpdate
eventDataType (EdConvCreate _) = ConvCreate
eventDataType (EdConvDelete _) = ConvDelete
eventDataType (EdCollaboratorAdd _ _) = CollaboratorAdd
eventDataType (EdCollaboratorUpdate _ _) = CollaboratorUpdate
eventDataType (EdCollaboratorRemove _) = CollaboratorRemove

parseEventData :: EventType -> Maybe Value -> Parser EventData
parseEventData MemberJoin Nothing = fail "missing event data for type 'team.member-join'"
Expand Down Expand Up @@ -226,6 +240,14 @@ parseEventData CollaboratorAdd Nothing = fail "missing event data for type 'team
parseEventData CollaboratorAdd (Just j) = do
let f o = EdCollaboratorAdd <$> o .: "user" <*> o .: "permissions"
withObject "collaborator add data" f j
parseEventData CollaboratorUpdate Nothing = fail "missing event data for type 'team.collaborator-update"
parseEventData CollaboratorUpdate (Just j) = do
let f o = EdCollaboratorUpdate <$> o .: "user" <*> o .: "permissions"
withObject "collaborator update data" f j
parseEventData CollaboratorRemove Nothing = fail "missing event data for type 'team.collaborator-remove"
parseEventData CollaboratorRemove (Just j) = do
let f o = EdCollaboratorRemove <$> o .: "user"
withObject "collaborator remove data" f j
parseEventData _ Nothing = pure EdTeamDelete
parseEventData t (Just _) = fail $ "unexpected event data for type " <> show t

Expand All @@ -240,5 +262,7 @@ genEventData = \case
ConvCreate -> EdConvCreate <$> arbitrary
ConvDelete -> EdConvDelete <$> arbitrary
CollaboratorAdd -> EdCollaboratorAdd <$> arbitrary <*> arbitrary
CollaboratorUpdate -> EdCollaboratorUpdate <$> arbitrary <*> arbitrary
CollaboratorRemove -> EdCollaboratorRemove <$> arbitrary

makeLenses ''Event
13 changes: 13 additions & 0 deletions libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,13 @@ type ITeamsAPIBase =
:> ReqBody '[JSON] NewTeamMember
:> MultiVerb1 'POST '[JSON] (RespondEmpty 200 "OK")
)
:<|> Named
"unchecked-remove-team-member"
( Summary
"Remove a user from a team and conversations"
:> ZLocalUser
:> MultiVerb1 'DELETE '[JSON] (RespondEmpty 200 "OK")
)
:<|> Named
"unchecked-get-team-members"
( QueryParam' '[Strict] "maxResults" (Range 1 HardTruncationLimit Int32)
Expand Down Expand Up @@ -305,6 +312,12 @@ type ITeamsAPIBase =
:> MultiVerb1 'PUT '[JSON] (RespondEmpty 204 "OK")
)
)
:<|> Named
"close-conversations-from"
( "close-conversations-from"
:> Capture "uid" UserId
:> MultiVerb1 'POST '[JSON] (RespondEmpty 200 "OK")
)

type IFeatureStatusGet cfg =
Named
Expand Down
23 changes: 23 additions & 0 deletions libs/wire-api/src/Wire/API/Routes/Public/Brig.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2015,6 +2015,29 @@ type TeamsAPI =
:> ReqBody '[JSON] NewTeamCollaborator
:> MultiVerb1 'POST '[JSON] (RespondEmpty 200 "")
)
:<|> Named
"update-team-collaborator"
( Summary "Update a collaborator permissions from the team."
:> From 'V11
:> ZLocalUser
:> "teams"
:> Capture "tid" TeamId
:> "collaborators"
:> Capture "uid" UserId
:> ReqBody '[JSON] (Set CollaboratorPermission)
:> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "")
)
:<|> Named
"remove-team-collaborator"
( Summary "Remove a collaborator from the team."
:> From 'V11
:> ZLocalUser
:> "teams"
:> Capture "tid" TeamId
:> "collaborators"
:> Capture "uid" UserId
:> MultiVerb1 'DELETE '[JSON] (RespondEmpty 200 "")
)
:<|> Named
"get-team-collaborators"
( Summary "Get all collaborators of the team."
Expand Down
6 changes: 5 additions & 1 deletion libs/wire-api/src/Wire/API/Team/Member.hs
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,8 @@ data HiddenPerm
| ChangeTeamMemberProfiles
| SearchContacts
| NewTeamCollaborator
| UpdateTeamCollaborator
| RemoveTeamCollaborator
deriving (Eq, Ord, Show)

-- | See Note [hidden team roles]
Expand Down Expand Up @@ -562,7 +564,9 @@ roleHiddenPermissions role = HiddenPermissions p p
CreateUpdateDeleteIdp,
CreateReadDeleteScimToken,
DownloadTeamMembersCsv,
NewTeamCollaborator
NewTeamCollaborator,
UpdateTeamCollaborator,
RemoveTeamCollaborator
]
roleHiddenPerms RoleMember =
(roleHiddenPerms RoleExternalPartner <>) $
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

module Wire.ConversationStore.Cassandra
( interpretConversationStoreToCassandra,
deleteConversation,
)
where

Expand Down
11 changes: 11 additions & 0 deletions libs/wire-subsystems/src/Wire/ConversationsSubsystem.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{-# LANGUAGE TemplateHaskell #-}

module Wire.ConversationsSubsystem where

import Data.Id
import Polysemy

data ConversationsSubsystem m a where
InternalCloseConversationsFrom :: TeamId -> UserId -> ConversationsSubsystem m ()

makeSem ''ConversationsSubsystem
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module Wire.ConversationsSubsystem.GalleyAPI
( interpretConversationsSubsystemToGalleyAPI,
)
where

import Imports
import Polysemy
import Wire.ConversationsSubsystem
import Wire.GalleyAPIAccess (GalleyAPIAccess)
import Wire.GalleyAPIAccess qualified as GalleyAPIAccess

interpretConversationsSubsystemToGalleyAPI :: (Member GalleyAPIAccess r) => InterpreterFor ConversationsSubsystem r
interpretConversationsSubsystemToGalleyAPI =
interpret $
\case
InternalCloseConversationsFrom tid uid -> GalleyAPIAccess.closeConversationsFrom tid uid
6 changes: 6 additions & 0 deletions libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ data GalleyAPIAccess m a where
Maybe (UserId, UTCTimeMillis) ->
Role ->
GalleyAPIAccess m Bool
RemoveTeamMember ::
Local UserId ->
UserId ->
TeamId ->
GalleyAPIAccess m ()
CreateTeam ::
UserId ->
NewTeam ->
Expand Down Expand Up @@ -139,5 +144,6 @@ data GalleyAPIAccess m a where
UserId ->
GalleyAPIAccess m [EJPDConvInfo]
GetTeamAdmins :: TeamId -> GalleyAPIAccess m Team.TeamMemberList
CloseConversationsFrom :: TeamId -> UserId -> GalleyAPIAccess m ()

makeSem ''GalleyAPIAccess
44 changes: 44 additions & 0 deletions libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ interpretGalleyAPIAccessToRpc disabledVersions galleyEndpoint =
NewClient id' ci -> newClient id' ci
CheckUserCanJoinTeam id' -> checkUserCanJoinTeam id'
AddTeamMember id' id'' a b -> addTeamMember id' id'' a b
RemoveTeamMember zUser' user team -> removeTeamMember zUser' user team
CreateTeam id' bnt id'' -> createTeam id' bnt id''
GetTeamMember id' id'' -> getTeamMember id' id''
GetTeamMembers tid maxResults -> getTeamMembers tid maxResults
Expand All @@ -93,6 +94,7 @@ interpretGalleyAPIAccessToRpc disabledVersions galleyEndpoint =
UnblockConversation lusr mconn qcnv -> unblockConversation v lusr mconn qcnv
GetEJPDConvInfo uid -> getEJPDConvInfo uid
GetTeamAdmins tid -> getTeamAdmins tid
CloseConversationsFrom tid uid -> closeConversationsFrom tid uid

getUserLegalholdStatus ::
( Member TinyLog r,
Expand Down Expand Up @@ -279,6 +281,29 @@ addTeamMember u tid minvmeta role = do
. expect [status200, status403]
. lbytes (encode bdy)

-- | Calls 'Galley.API.uncheckedRemoveTeamMemberH'.
removeTeamMember ::
( Member Rpc r,
Member (Input Endpoint) r,
Member TinyLog r
) =>
Local UserId ->
UserId ->
TeamId ->
Sem r ()
removeTeamMember _puid tuid tid = do
debug $
remote "galley"
. msg (val "Removing member from team")
void $ galleyRequest req
where
req =
method DELETE
. paths ["i", "teams", toByteString' tid, "members"]
. header "Content-Type" "application/json"
. zUser tuid
. expect [status200, status403]

-- | Calls 'Galley.API.createBindingTeamH'.
createTeam ::
( Member Rpc r,
Expand Down Expand Up @@ -656,3 +681,22 @@ getEJPDConvInfo uid = do
getReq =
method GET
. paths ["i", "user", toByteString' uid, "all-conversations"]

-- | Calls 'Galley.API.updateTeamStatusH'.
closeConversationsFrom ::
( Member Rpc r,
Member (Input Endpoint) r,
Member TinyLog r
) =>
TeamId ->
UserId ->
Sem r ()
closeConversationsFrom tid uid = do
debug $ remote "galley" . msg (val "Close all conversations of a user in a team")
void $ galleyRequest req
where
req =
method POST
. paths ["i", "teams", toByteString' tid, "close-conversations-from", toByteString' uid]
. header "Content-Type" "application/json"
. expect2xx
2 changes: 2 additions & 0 deletions libs/wire-subsystems/src/Wire/TeamCollaboratorsStore.hs
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@ data TeamCollaboratorsStore m a where
GetTeamCollaborator :: TeamId -> UserId -> TeamCollaboratorsStore m (Maybe TeamCollaborator)
GetTeamCollaborations :: UserId -> TeamCollaboratorsStore m ([TeamCollaborator])
GetTeamCollaboratorsWithIds :: Set TeamId -> Set UserId -> TeamCollaboratorsStore m [TeamCollaborator]
UpdateTeamCollaborator :: UserId -> TeamId -> Set CollaboratorPermission -> TeamCollaboratorsStore m ()
RemoveTeamCollaborator :: UserId -> TeamId -> TeamCollaboratorsStore m ()

makeSem ''TeamCollaboratorsStore
Loading