From 372f6107b8c6caebc8efe950cacc6a179ecea83e Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 18 May 2022 09:56:44 +0000 Subject: [PATCH 1/6] failing integration test --- libs/wire-api/src/Wire/API/Team/Export.hs | 10 ++++++--- services/galley/src/Galley/API/Teams.hs | 3 ++- services/galley/test/integration/API/Teams.hs | 21 ++++++++++++++++++- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Team/Export.hs b/libs/wire-api/src/Wire/API/Team/Export.hs index 4cffed4cf2..ac9cd8a824 100644 --- a/libs/wire-api/src/Wire/API/Team/Export.hs +++ b/libs/wire-api/src/Wire/API/Team/Export.hs @@ -49,7 +49,8 @@ data TeamExportUser = TeamExportUser tExportSAMLNamedId :: Text, -- If SAML IdP and SCIM peer are set up correctly, 'tExportSAMLNamedId' and 'tExportSCIMExternalId' always align. tExportSCIMExternalId :: Text, tExportSCIMRichInfo :: Maybe RichInfo, - tExportUserId :: UserId + tExportUserId :: UserId, + tExportNumDevices :: Int } deriving (Show, Eq, Generic) deriving (Arbitrary) via (GenericUniform TeamExportUser) @@ -68,7 +69,8 @@ instance ToNamedRecord TeamExportUser where ("saml_name_id", secureCsvFieldToByteString (tExportSAMLNamedId row)), ("scim_external_id", secureCsvFieldToByteString (tExportSCIMExternalId row)), ("scim_rich_info", maybe "" (cs . Aeson.encode) (tExportSCIMRichInfo row)), - ("user_id", secureCsvFieldToByteString (tExportUserId row)) + ("user_id", secureCsvFieldToByteString (tExportUserId row)), + ("num_devices", secureCsvFieldToByteString (tExportNumDevices row)) ] secureCsvFieldToByteString :: forall a. ToByteString a => a -> ByteString @@ -89,7 +91,8 @@ instance DefaultOrdered TeamExportUser where "saml_name_id", "scim_external_id", "scim_rich_info", - "user_id" + "user_id", + "num_devices" ] allowEmpty :: (ByteString -> Parser a) -> ByteString -> Parser (Maybe a) @@ -117,6 +120,7 @@ instance FromNamedRecord TeamExportUser where <*> (nrec .: "scim_external_id" >>= parseByteString) <*> (nrec .: "scim_rich_info" >>= allowEmpty (maybe (fail "failed to decode RichInfo") pure . Aeson.decode . cs)) <*> (nrec .: "user_id" >>= parseByteString) + <*> (nrec .: "num_devices" >>= parseByteString) quoted :: ByteString -> ByteString quoted bs = case C.uncons bs of diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index 283279c6d3..02b5e6e8bc 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -566,7 +566,8 @@ getTeamMembersCSVH (zusr ::: tid ::: _) = do tExportSAMLNamedId = fromMaybe "" (samlNamedId user), tExportSCIMExternalId = fromMaybe "" (userSCIMExternalId user), tExportSCIMRichInfo = richInfos uid, - tExportUserId = U.userId user + tExportUserId = U.userId user, + tExportNumDevices = -1 } lookupInviterHandle :: Member BrigAccess r => [TeamMember] -> Sem r (UserId -> Maybe Handle.Handle) diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index 3803e17628..4dd4f6d622 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -86,6 +86,9 @@ import qualified Wire.API.Team.Member as Member import qualified Wire.API.Team.Member as TM import qualified Wire.API.User as Public import qualified Wire.API.User as U +import qualified Wire.API.User.Client as C +import qualified Wire.API.User.Client.Prekey as PC + tests :: IO TestSetup -> TestTree tests s = @@ -286,7 +289,8 @@ testListTeamMembersCsv :: HasCallStack => Int -> TestM () testListTeamMembersCsv numMembers = do let teamSize = numMembers + 1 - (owner, tid, _mbs) <- Util.createBindingTeamWithNMembersWithHandles True numMembers + (owner, tid, mbs) <- Util.createBindingTeamWithNMembersWithHandles True numMembers + addClients (owner : mbs) resp <- Util.getTeamMembersCsv owner tid let rbody = fromMaybe (error "no body") . responseBody $ resp usersInCsv <- either (error "could not decode csv") pure (decodeCSV @TeamExportUser rbody) @@ -322,6 +326,8 @@ testListTeamMembersCsv numMembers = do assertEqual ("tExportIdpIssuer: " <> show (U.userId user)) (userToIdPIssuer user) (tExportIdpIssuer export) assertEqual ("tExportManagedBy: " <> show (U.userId user)) (U.userManagedBy user) (tExportManagedBy export) assertEqual ("tExportUserId: " <> show (U.userId user)) (U.userId user) (tExportUserId export) + assertEqual ("tExportNumDevices: ") 1 (tExportNumDevices export) + where userToIdPIssuer :: HasCallStack => U.User -> Maybe HttpsUrl userToIdPIssuer usr = case (U.userIdentity >=> U.ssoIdentity) usr of @@ -334,6 +340,19 @@ testListTeamMembersCsv numMembers = do countOn :: Eq b => (a -> b) -> b -> [a] -> Int countOn prop val xs = sum $ fmap (bool 0 1 . (== val) . prop) xs + + addClients :: [UserId] -> TestM () + addClients xs = forM_ xs addClient + + addClient :: UserId -> TestM () + addClient uid = do + brig <- view tsBrig + post (brig . paths ["i", "clients", toByteString' uid] . contentJson . json newClient) !!! const 201 === statusCode + + newClient :: C.NewClient + newClient = + let lpk = PC.lastPrekey "pQABARn//wKhAFggnCcZIK1pbtlJf4wRQ44h4w7/sfSgj5oWXMQaUGYAJ/sDoQChAFgglacihnqg/YQJHkuHNFU7QD6Pb3KN4FnubaCF2EVOgRkE9g==" + in C.newClient C.PermanentClientType lpk testListTeamMembersTruncated :: TestM () testListTeamMembersTruncated = do From 8d36a4037845bcd33c14201b7cdf76585ea21c74 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 18 May 2022 10:56:56 +0000 Subject: [PATCH 2/6] impl --- services/galley/src/Galley/API/Teams.hs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index 02b5e6e8bc..1c9b0ce032 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -519,10 +519,11 @@ getTeamMembersCSVH (zusr ::: tid ::: _) = do lookupUser <$> E.lookupActivatedUsers (fmap (view userId) members) richInfos <- lookupRichInfo <$> E.getRichInfoMultiUser (fmap (view userId) members) + numUserClients <- lookupClients <$> E.lookupClients (fmap (view userId) members) output @LByteString ( encodeDefaultOrderedByNameWith defaultEncodeOptions - (mapMaybe (teamExportUser users inviters richInfos) members) + (mapMaybe (teamExportUser users inviters richInfos numUserClients) members) ) pure $ responseStream @@ -548,9 +549,10 @@ getTeamMembersCSVH (zusr ::: tid ::: _) = do (UserId -> Maybe User) -> (UserId -> Maybe Handle.Handle) -> (UserId -> Maybe RichInfo) -> + (UserId -> Int) -> TeamMember -> Maybe TeamExportUser - teamExportUser users inviters richInfos member = do + teamExportUser users inviters richInfos numClients member = do let uid = member ^. userId user <- users uid pure $ @@ -567,7 +569,7 @@ getTeamMembersCSVH (zusr ::: tid ::: _) = do tExportSCIMExternalId = fromMaybe "" (userSCIMExternalId user), tExportSCIMRichInfo = richInfos uid, tExportUserId = U.userId user, - tExportNumDevices = -1 + tExportNumDevices = numClients uid } lookupInviterHandle :: Member BrigAccess r => [TeamMember] -> Sem r (UserId -> Maybe Handle.Handle) @@ -596,6 +598,9 @@ getTeamMembersCSVH (zusr ::: tid ::: _) = do lookupRichInfo :: [(UserId, RichInfo)] -> (UserId -> Maybe RichInfo) lookupRichInfo pairs = (`M.lookup` M.fromList pairs) + lookupClients :: Conv.UserClients -> UserId -> Int + lookupClients userClients uid = maybe 0 length (M.lookup uid (Conv.userClients userClients)) + samlNamedId :: User -> Maybe Text samlNamedId = userSSOId >=> \case From f7254048e575162ad709141386ee1a881c69dbea Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 18 May 2022 10:59:37 +0000 Subject: [PATCH 3/6] changelog --- changelog.d/2-features/pr-2407 | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2-features/pr-2407 diff --git a/changelog.d/2-features/pr-2407 b/changelog.d/2-features/pr-2407 new file mode 100644 index 0000000000..3af661fc26 --- /dev/null +++ b/changelog.d/2-features/pr-2407 @@ -0,0 +1 @@ +CSV export in team management now includes the number of devices per user From 36fa24aeb29208b3d5a0d2be59da9cebf5a6d842 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 18 May 2022 11:30:27 +0000 Subject: [PATCH 4/6] correct formatting --- services/galley/test/integration/API/Teams.hs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index 4dd4f6d622..ab03af1756 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -89,7 +89,6 @@ import qualified Wire.API.User as U import qualified Wire.API.User.Client as C import qualified Wire.API.User.Client.Prekey as PC - tests :: IO TestSetup -> TestTree tests s = testGroup "Teams API" $ @@ -327,7 +326,6 @@ testListTeamMembersCsv numMembers = do assertEqual ("tExportManagedBy: " <> show (U.userId user)) (U.userManagedBy user) (tExportManagedBy export) assertEqual ("tExportUserId: " <> show (U.userId user)) (U.userId user) (tExportUserId export) assertEqual ("tExportNumDevices: ") 1 (tExportNumDevices export) - where userToIdPIssuer :: HasCallStack => U.User -> Maybe HttpsUrl userToIdPIssuer usr = case (U.userIdentity >=> U.ssoIdentity) usr of @@ -340,7 +338,7 @@ testListTeamMembersCsv numMembers = do countOn :: Eq b => (a -> b) -> b -> [a] -> Int countOn prop val xs = sum $ fmap (bool 0 1 . (== val) . prop) xs - + addClients :: [UserId] -> TestM () addClients xs = forM_ xs addClient @@ -350,9 +348,9 @@ testListTeamMembersCsv numMembers = do post (brig . paths ["i", "clients", toByteString' uid] . contentJson . json newClient) !!! const 201 === statusCode newClient :: C.NewClient - newClient = + newClient = let lpk = PC.lastPrekey "pQABARn//wKhAFggnCcZIK1pbtlJf4wRQ44h4w7/sfSgj5oWXMQaUGYAJ/sDoQChAFgglacihnqg/YQJHkuHNFU7QD6Pb3KN4FnubaCF2EVOgRkE9g==" - in C.newClient C.PermanentClientType lpk + in C.newClient C.PermanentClientType lpk testListTeamMembersTruncated :: TestM () testListTeamMembersTruncated = do From 9c1ff55c1a63cc4a23668f8197c931ed60668491 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 19 May 2022 14:40:43 +0000 Subject: [PATCH 5/6] different numbers of clients --- services/galley/test/integration/API/Teams.hs | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index ab03af1756..64510e7a08 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -46,6 +46,7 @@ import Data.Json.Util hiding ((#)) import qualified Data.LegalHold as LH import Data.List1 import qualified Data.List1 as List1 +import qualified Data.Map as Map import Data.Misc (HttpsUrl, PlainTextPassword (..), mkHttpsUrl) import Data.Qualified import Data.Range @@ -289,7 +290,8 @@ testListTeamMembersCsv numMembers = do let teamSize = numMembers + 1 (owner, tid, mbs) <- Util.createBindingTeamWithNMembersWithHandles True numMembers - addClients (owner : mbs) + let numClientMappings = Map.fromList $ (owner : mbs) `zip` (cycle [1, 2, 3] :: [Int]) + addClients numClientMappings resp <- Util.getTeamMembersCsv owner tid let rbody = fromMaybe (error "no body") . responseBody $ resp usersInCsv <- either (error "could not decode csv") pure (decodeCSV @TeamExportUser rbody) @@ -325,7 +327,7 @@ testListTeamMembersCsv numMembers = do assertEqual ("tExportIdpIssuer: " <> show (U.userId user)) (userToIdPIssuer user) (tExportIdpIssuer export) assertEqual ("tExportManagedBy: " <> show (U.userId user)) (U.userManagedBy user) (tExportManagedBy export) assertEqual ("tExportUserId: " <> show (U.userId user)) (U.userId user) (tExportUserId export) - assertEqual ("tExportNumDevices: ") 1 (tExportNumDevices export) + assertEqual ("tExportNumDevices: ") (Map.findWithDefault (-1) (U.userId user) numClientMappings) (tExportNumDevices export) where userToIdPIssuer :: HasCallStack => U.User -> Maybe HttpsUrl userToIdPIssuer usr = case (U.userIdentity >=> U.ssoIdentity) usr of @@ -339,18 +341,19 @@ testListTeamMembersCsv numMembers = do countOn :: Eq b => (a -> b) -> b -> [a] -> Int countOn prop val xs = sum $ fmap (bool 0 1 . (== val) . prop) xs - addClients :: [UserId] -> TestM () - addClients xs = forM_ xs addClient + addClients :: Map.Map UserId Int -> TestM () + addClients xs = forM_ (Map.toList xs) addClientForUser - addClient :: UserId -> TestM () - addClient uid = do + addClientForUser :: (UserId, Int) -> TestM () + addClientForUser (uid, n) = forM_ [0 .. (n -1)] (addClient uid) + + addClient :: UserId -> Int -> TestM () + addClient uid i = do brig <- view tsBrig - post (brig . paths ["i", "clients", toByteString' uid] . contentJson . json newClient) !!! const 201 === statusCode + post (brig . paths ["i", "clients", toByteString' uid] . contentJson . json (newClient (someLastPrekeys !! i)) . queryItem "skip_reauth" "true") !!! const 201 === statusCode - newClient :: C.NewClient - newClient = - let lpk = PC.lastPrekey "pQABARn//wKhAFggnCcZIK1pbtlJf4wRQ44h4w7/sfSgj5oWXMQaUGYAJ/sDoQChAFgglacihnqg/YQJHkuHNFU7QD6Pb3KN4FnubaCF2EVOgRkE9g==" - in C.newClient C.PermanentClientType lpk + newClient :: PC.LastPrekey -> C.NewClient + newClient lpk = C.newClient C.PermanentClientType lpk testListTeamMembersTruncated :: TestM () testListTeamMembersTruncated = do From c60f4f2761b0e030295f208bf5e09e6395df7381 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 20 May 2022 09:12:44 +0000 Subject: [PATCH 6/6] hi ci