Skip to content

Commit 909912b

Browse files
committed
WPB-19712: Allow team admin to update the channels to user-group association
1 parent 2233db3 commit 909912b

File tree

12 files changed

+185
-7
lines changed

12 files changed

+185
-7
lines changed

integration/test/API/Brig.hs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,6 +1061,11 @@ getUserGroup user gid = do
10611061
req <- baseRequest user Brig Versioned $ joinHttpPath ["user-groups", gid]
10621062
submit "GET" req
10631063

1064+
updateUserGroupChannels :: (MakesValue user) => user -> String -> [String] -> App Response
1065+
updateUserGroupChannels user gid convIds = do
1066+
req <- baseRequest user Brig Versioned $ joinHttpPath ["user-groups", gid, "channels"]
1067+
submit "PUT" $ req & addJSONObject ["channels" .= convIds]
1068+
10641069
data GetUserGroupsArgs = GetUserGroupsArgs
10651070
{ q :: Maybe String,
10661071
sortByKeys :: Maybe String,

integration/test/Test/UserGroup.hs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,3 +373,56 @@ testUserGroupMembersCount = do
373373
resp.status `shouldMatchInt` 200
374374
resp.json %. "page.0.membersCount" `shouldMatchInt` 2
375375
resp.json %. "total" `shouldMatchInt` 1
376+
377+
testUserGroupUpdateChannels :: (HasCallStack) => App ()
378+
testUserGroupUpdateChannels = do
379+
(alice, tid, [_bob]) <- createTeam OwnDomain 2
380+
381+
ug <-
382+
createUserGroup alice (object ["name" .= "none", "members" .= (mempty :: [String])])
383+
>>= getJSON 200
384+
gid <- ug %. "id" & asString
385+
386+
convId <-
387+
postConversation alice (defProteus {team = Just tid})
388+
>>= getJSON 201
389+
>>= objConvId
390+
updateUserGroupChannels alice gid [convId.id_] >>= assertSuccess
391+
392+
-- bobId <- asString $ bob %. "id"
393+
bindResponse (getUserGroup alice gid) $ \resp -> do
394+
resp.status `shouldMatchInt` 200
395+
396+
-- FUTUREWORK: check the actual associated channels
397+
-- resp.json %. "members" `shouldMatch` [bobId]
398+
399+
testUserGroupUpdateChannelsNonAdmin :: (HasCallStack) => App ()
400+
testUserGroupUpdateChannelsNonAdmin = do
401+
(alice, tid, [bob]) <- createTeam OwnDomain 2
402+
403+
ug <-
404+
createUserGroup alice (object ["name" .= "none", "members" .= (mempty :: [String])])
405+
>>= getJSON 200
406+
gid <- ug %. "id" & asString
407+
408+
convId <-
409+
postConversation alice (defProteus {team = Just tid})
410+
>>= getJSON 201
411+
>>= objConvId
412+
updateUserGroupChannels bob gid [convId.id_] >>= assertLabel 404 "user-group-not-found"
413+
414+
testUserGroupUpdateChannelsNonExisting :: (HasCallStack) => App ()
415+
testUserGroupUpdateChannelsNonExisting = do
416+
(alice, tid, _) <- createTeam OwnDomain 1
417+
(bob, _, _) <- createTeam OwnDomain 1
418+
419+
ug <-
420+
createUserGroup alice (object ["name" .= "none", "members" .= (mempty :: [String])])
421+
>>= getJSON 200
422+
gid <- ug %. "id" & asString
423+
424+
convId <-
425+
postConversation alice (defProteus {team = Just tid})
426+
>>= getJSON 201
427+
>>= objConvId
428+
updateUserGroupChannels bob gid [convId.id_] >>= assertLabel 404 "user-group-not-found"

libs/wire-api/src/Wire/API/Error/Brig.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ data BrigError
118118
| UserGroupNotFound
119119
| UserGroupNotATeamAdmin
120120
| UserGroupMemberIsNotInTheSameTeam
121+
| UserGroupChannelNotFound
121122
| DuplicateEntry
122123
| MLSInvalidLeafNodeSignature
123124

@@ -351,6 +352,8 @@ type instance MapError 'UserGroupNotFound = 'StaticError 404 "user-group-not-fou
351352

352353
type instance MapError 'UserGroupNotATeamAdmin = 'StaticError 403 "user-group-write-forbidden" "Only team admins can create, update, or delete user groups."
353354

355+
type instance MapError 'UserGroupChannelNotFound = 'StaticError 404 "user-group-channel-not-found" "Specified Channel does not exists or does not belongs to the team"
356+
354357
type instance MapError 'UserGroupMemberIsNotInTheSameTeam = 'StaticError 400 "user-group-invalid" "Only team members of the same team can be added to a user group."
355358

356359
type instance MapError 'DuplicateEntry = 'StaticError 409 "duplicate-entry" "Entry already exists"

libs/wire-api/src/Wire/API/Routes/Public/Brig.hs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,8 +418,11 @@ type UserGroupAPI =
418418
)
419419
:<|> Named
420420
"update-user-group-channels"
421-
( Summary "[STUB] Update user group channels. Replaces the channels with the given list."
421+
( Summary "Replaces the channels with the given list."
422422
:> From 'V12
423+
:> CanThrow 'UserGroupNotFound
424+
:> CanThrow 'UserGroupNotATeamAdmin
425+
:> CanThrow 'UserGroupNotFound
423426
:> ZLocalUser
424427
:> "user-groups"
425428
:> Capture "gid" UserGroupId
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
CREATE TABLE public.user_group_channel (
2+
user_group_id uuid NOT NULL,
3+
conv_id uuid NOT NULL,
4+
PRIMARY KEY (user_group_id, conv_id)
5+
);
6+
7+
ALTER TABLE ONLY public.user_group_channel
8+
ADD CONSTRAINT fk_user_group_channel FOREIGN KEY (user_group_id) REFERENCES public.user_group(id) ON DELETE CASCADE;

libs/wire-subsystems/src/Wire/UserGroupStore.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,6 @@ data UserGroupStore m a where
3737
AddUser :: UserGroupId -> UserId -> UserGroupStore m ()
3838
UpdateUsers :: UserGroupId -> Vector UserId -> UserGroupStore m ()
3939
RemoveUser :: UserGroupId -> UserId -> UserGroupStore m ()
40+
UpdateUserGroupChannels :: UserGroupId -> Vector ConvId -> UserGroupStore m ()
4041

4142
makeSem ''UserGroupStore

libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import Polysemy.Error (Error, throw)
2929
import Polysemy.Input
3030
import Wire.API.Pagination
3131
import Wire.API.User.Profile
32-
import Wire.API.UserGroup
32+
import Wire.API.UserGroup hiding (UpdateUserGroupChannels)
3333
import Wire.API.UserGroup.Pagination
3434
import Wire.UserGroupStore (PaginationState (..), UserGroupPageRequest (..), UserGroupStore (..), toSortBy)
3535

@@ -53,6 +53,7 @@ interpretUserGroupStoreToPostgres =
5353
AddUser gid uid -> addUser gid uid
5454
UpdateUsers gid uids -> updateUsers gid uids
5555
RemoveUser gid uid -> removeUser gid uid
56+
UpdateUserGroupChannels gid convIds -> updateUserGroupChannels gid convIds
5657

5758
updateUsers :: (UserGroupStorePostgresEffectConstraints r) => UserGroupId -> Vector UserId -> Sem r ()
5859
updateUsers gid uids = do
@@ -408,6 +409,38 @@ removeUser =
408409
delete from user_group_member where user_group_id = ($1 :: uuid) and user_id = ($2 :: uuid)
409410
|]
410411

412+
updateUserGroupChannels ::
413+
forall r.
414+
(UserGroupStorePostgresEffectConstraints r) =>
415+
UserGroupId ->
416+
Vector ConvId ->
417+
Sem r ()
418+
updateUserGroupChannels gid convIds = do
419+
pool <- input
420+
eitherErrorOrUnit <- liftIO $ use pool session
421+
either throw pure eitherErrorOrUnit
422+
where
423+
session :: Session ()
424+
session = TxSessions.transaction TxSessions.Serializable TxSessions.Write $ do
425+
Tx.statement (gid, convIds) deleteStatement
426+
Tx.statement (gid, convIds) insertStatement
427+
428+
deleteStatement :: Statement (UserGroupId, Vector ConvId) ()
429+
deleteStatement =
430+
lmap
431+
(bimap toUUID (fmap toUUID))
432+
$ [resultlessStatement|
433+
delete from user_group_channel where user_group_id = ($1 :: uuid) and conv_id not in (SELECT unnest($2 :: uuid[]))
434+
|]
435+
436+
insertStatement :: Statement (UserGroupId, Vector ConvId) ()
437+
insertStatement =
438+
lmap (bimap (fmap (.toUUID)) (fmap (.toUUID)) . uncurry toRelationTable) $
439+
[resultlessStatement|
440+
insert into user_group_channel (user_group_id, conv_id) select * from unnest ($1 :: uuid[], $2 :: uuid[])
441+
on conflict (user_group_id, conv_id) do nothing
442+
|]
443+
411444
crudUser ::
412445
forall r.
413446
(UserGroupStorePostgresEffectConstraints r) =>

libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,6 @@ data UserGroupSubsystem m a where
3232
AddUsers :: UserId -> UserGroupId -> Vector UserId -> UserGroupSubsystem m ()
3333
UpdateUsers :: UserId -> UserGroupId -> Vector UserId -> UserGroupSubsystem m ()
3434
RemoveUser :: UserId -> UserGroupId -> UserId -> UserGroupSubsystem m ()
35+
UpdateChannels :: UserId -> UserGroupId -> Vector ConvId -> UserGroupSubsystem m ()
3536

3637
makeSem ''UserGroupSubsystem

libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import Wire.API.UserEvent
2323
import Wire.API.UserGroup
2424
import Wire.API.UserGroup.Pagination
2525
import Wire.Error
26+
import Wire.GalleyAPIAccess (GalleyAPIAccess, getTeamConv)
2627
import Wire.NotificationSubsystem
2728
import Wire.TeamSubsystem
2829
import Wire.UserGroupStore (PaginationState (..), UserGroupPageRequest (..))
@@ -36,7 +37,8 @@ interpretUserGroupSubsystem ::
3637
Member Store.UserGroupStore r,
3738
Member (Input (Local ())) r,
3839
Member NotificationSubsystem r,
39-
Member TeamSubsystem r
40+
Member TeamSubsystem r,
41+
Member GalleyAPIAccess r
4042
) =>
4143
InterpreterFor UserGroupSubsystem r
4244
interpretUserGroupSubsystem = interpret $ \case
@@ -50,11 +52,13 @@ interpretUserGroupSubsystem = interpret $ \case
5052
AddUsers adder groupId addeeIds -> addUsers adder groupId addeeIds
5153
UpdateUsers updater groupId uids -> updateUsers updater groupId uids
5254
RemoveUser remover groupId removeeId -> removeUser remover groupId removeeId
55+
UpdateChannels performer groupId channelIds -> updateChannels performer groupId channelIds
5356

5457
data UserGroupSubsystemError
5558
= UserGroupNotATeamAdmin
5659
| UserGroupMemberIsNotInTheSameTeam
5760
| UserGroupNotFound
61+
| UserGroupChannelNotFound
5862
deriving (Show, Eq)
5963

6064
userGroupSubsystemErrorToHttpError :: UserGroupSubsystemError -> HttpError
@@ -63,6 +67,7 @@ userGroupSubsystemErrorToHttpError =
6367
UserGroupNotATeamAdmin -> errorToWai @E.UserGroupNotATeamAdmin
6468
UserGroupMemberIsNotInTheSameTeam -> errorToWai @E.UserGroupMemberIsNotInTheSameTeam
6569
UserGroupNotFound -> errorToWai @E.UserGroupNotFound
70+
UserGroupChannelNotFound -> errorToWai @E.UserGroupChannelNotFound
6671

6772
createUserGroup ::
6873
( Member UserSubsystem r,
@@ -328,3 +333,20 @@ removeUser remover groupId removeeId = do
328333
pushNotifications
329334
[ mkEvent remover (UserGroupUpdated groupId) admins
330335
]
336+
337+
updateChannels ::
338+
( Member UserSubsystem r,
339+
Member Store.UserGroupStore r,
340+
Member (Error UserGroupSubsystemError) r,
341+
Member TeamSubsystem r,
342+
Member GalleyAPIAccess r
343+
) =>
344+
UserId ->
345+
UserGroupId ->
346+
Vector ConvId ->
347+
Sem r ()
348+
updateChannels performer groupId channelIds = do
349+
void $ getUserGroup performer groupId >>= note UserGroupNotFound
350+
teamId <- getTeamAsAdmin performer >>= note UserGroupNotATeamAdmin
351+
traverse_ (getTeamConv performer teamId >=> note UserGroupChannelNotFound) channelIds
352+
Store.updateUserGroupChannels groupId channelIds

libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
module Wire.MockInterpreters.UserGroupStore where
88

99
import Control.Lens ((%~), _2)
10+
import Data.Domain (Domain (Domain))
1011
import Data.Id
1112
import Data.Json.Util
1213
import Data.Map qualified as Map
14+
import Data.Qualified (Qualified (Qualified))
1315
import Data.Text qualified as T
1416
import Data.Time.Clock
1517
import Data.Vector (Vector, fromList)
@@ -21,7 +23,7 @@ import Polysemy.State
2123
import System.Random (StdGen, mkStdGen)
2224
import Wire.API.Pagination
2325
import Wire.API.User
24-
import Wire.API.UserGroup
26+
import Wire.API.UserGroup hiding (UpdateUserGroupChannels)
2527
import Wire.API.UserGroup.Pagination
2628
import Wire.MockInterpreters.Now
2729
import Wire.MockInterpreters.Random
@@ -62,6 +64,7 @@ userGroupStoreTestInterpreter =
6264
AddUser gid uid -> addUserImpl gid uid
6365
UpdateUsers gid uids -> updateUsersImpl gid uids
6466
RemoveUser gid uid -> removeUserImpl gid uid
67+
UpdateUserGroupChannels gid convIds -> updateUserGroupChannelsImpl gid convIds
6568

6669
updateUsersImpl :: (UserGroupStoreInMemEffectConstraints r) => UserGroupId -> Vector UserId -> Sem r ()
6770
updateUsersImpl gid uids = do
@@ -179,6 +182,25 @@ removeUserImpl gid uid = do
179182

180183
modifyUserGroupsGidOnly gid (Map.alter f)
181184

185+
updateUserGroupChannelsImpl ::
186+
(UserGroupStoreInMemEffectConstraints r) =>
187+
UserGroupId ->
188+
Vector ConvId ->
189+
Sem r ()
190+
updateUserGroupChannelsImpl gid convIds = do
191+
let f :: Maybe UserGroup -> Maybe UserGroup
192+
f Nothing = Nothing
193+
f (Just g) =
194+
Just
195+
( g
196+
{ channels = Identity $ Just $ flip Qualified (Domain "<local>") <$> convIds,
197+
channelsCount = Just $ length convIds
198+
} ::
199+
UserGroup
200+
)
201+
202+
modifyUserGroupsGidOnly gid (Map.alter f)
203+
182204
----------------------------------------------------------------------
183205

184206
modifyUserGroupsGidOnly ::

0 commit comments

Comments
 (0)