Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 cabal.project
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ packages:
, tools/db/move-team/
, tools/db/phone-users/
, tools/db/repair-handles/
, tools/db/team-info/
, tools/db/repair-brig-clients-table/
, tools/db/service-backfill/
, tools/fedcalls/
Expand Down
1 change: 1 addition & 0 deletions changelog.d/5-internal/WPB-11301-db-tool-team-info
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tools/db/team-info: collects last login times of all team members
1 change: 1 addition & 0 deletions nix/local-haskell-packages.nix
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
repair-brig-clients-table = hself.callPackage ../tools/db/repair-brig-clients-table/default.nix { inherit gitignoreSource; };
repair-handles = hself.callPackage ../tools/db/repair-handles/default.nix { inherit gitignoreSource; };
service-backfill = hself.callPackage ../tools/db/service-backfill/default.nix { inherit gitignoreSource; };
team-info = hself.callPackage ../tools/db/team-info/default.nix { inherit gitignoreSource; };
fedcalls = hself.callPackage ../tools/fedcalls/default.nix { inherit gitignoreSource; };
mlsstats = hself.callPackage ../tools/mlsstats/default.nix { inherit gitignoreSource; };
rabbitmq-consumer = hself.callPackage ../tools/rabbitmq-consumer/default.nix { inherit gitignoreSource; };
Expand Down
1 change: 1 addition & 0 deletions nix/wire-server.nix
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ let
integration = [ "integration" ];
rabbitmq-consumer = [ "rabbitmq-consumer" ];
test-stats = [ "test-stats" ];
team-info = [ "team-info" ];
};

inherit (lib) attrsets;
Expand Down
1 change: 1 addition & 0 deletions tools/db/team-info/.ormolu
48 changes: 48 additions & 0 deletions tools/db/team-info/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Team info

This program scans brig's and galley's cassandra for members of a team, their clients, and those clients' last access times.

Useful for finding out which accounts you don't want to pay license fees any more.

Example usage:

```shell
team-info \
--brig-cassandra-port 9048 --brig-cassandra-keyspace brig \
--galley-cassandra-port 9049 --galley-cassandra-keyspace galley \
--team-id=904912aa-7c10-11ef-9c85-8bfd758593f6
```

Display usage:

```shell
team-info -h
```

```text
team-info

Usage: team-info [--brig-cassandra-host HOST] [--brig-cassandra-port PORT]
[--brig-cassandra-keyspace STRING]
[--galley-cassandra-host HOST] [--galley-cassandra-port PORT]
[--galley-cassandra-keyspace STRING] (-t|--team-id ID)

get team info

Available options:
-h,--help Show this help text
--brig-cassandra-host HOST
Cassandra Host for brig (default: "localhost")
--brig-cassandra-port PORT
Cassandra Port for brig (default: 9042)
--brig-cassandra-keyspace STRING
Cassandra Keyspace for brig (default: "brig_test")
--galley-cassandra-host HOST
Cassandra Host for galley (default: "localhost")
--galley-cassandra-port PORT
Cassandra Port for galley (default: 9043)
--galley-cassandra-keyspace STRING
Cassandra Keyspace for galley
(default: "galley_test")
-t,--team-id ID Team ID
```
23 changes: 23 additions & 0 deletions tools/db/team-info/app/Main.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2024 Wire Swiss GmbH <opensource@wire.com>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

module Main where

import qualified TeamInfo.Lib as Lib

main :: IO ()
main = Lib.main
40 changes: 40 additions & 0 deletions tools/db/team-info/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# WARNING: GENERATED FILE, DO NOT EDIT.
# This file is generated by running hack/bin/generate-local-nix-packages.sh and
# must be regenerated whenever local packages are added or removed, or
# dependencies are added or removed.
{ mkDerivation
, base
, cassandra-util
, conduit
, cql
, gitignoreSource
, imports
, lens
, lib
, optparse-applicative
, time
, tinylog
, types-common
}:
mkDerivation {
pname = "team-info";
version = "1.0.0";
src = gitignoreSource ./.;
isLibrary = true;
isExecutable = true;
libraryHaskellDepends = [
cassandra-util
conduit
cql
imports
lens
optparse-applicative
time
tinylog
types-common
];
executableHaskellDepends = [ base ];
description = "get team info from cassandra";
license = lib.licenses.agpl3Only;
mainProgram = "team-info";
}
93 changes: 93 additions & 0 deletions tools/db/team-info/src/TeamInfo/Lib.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
{-# LANGUAGE OverloadedStrings #-}

-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2024 Wire Swiss GmbH <opensource@wire.com>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

module TeamInfo.Lib where

import Cassandra as C
import Cassandra.Settings as C
import Data.Conduit
import qualified Data.Conduit.Combinators as Conduit
import qualified Data.Conduit.List as CL
import Data.Id (TeamId, UserId)
import Data.Time
import qualified Database.CQL.Protocol as CQL
import Imports
import Options.Applicative
import qualified System.Logger as Log
import TeamInfo.Types

lookupClientsLastActiveTimestamps :: ClientState -> UserId -> IO [Maybe UTCTime]
lookupClientsLastActiveTimestamps client u = do
runClient client $ runIdentity <$$> retry x1 (query selectClients (params One (Identity u)))
where
selectClients :: PrepQuery R (Identity UserId) (Identity (Maybe UTCTime))
selectClients = "SELECT last_active from clients where user = ?"

selectTeamMembers :: ClientState -> TeamId -> ConduitM () [TeamMemberRow] IO ()
selectTeamMembers client teamId =
transPipe (runClient client) (paginateC cql (paramsP One (Identity teamId) 1000) x5)
.| Conduit.map (fmap CQL.asRecord)
where
cql :: C.PrepQuery C.R (Identity TeamId) (CQL.TupleType TeamMemberRow)
cql =
"SELECT user, legalhold_status FROM team_member WHERE team = ?"

lookUpActivity :: ClientState -> TeamMemberRow -> IO TeamMember
lookUpActivity brigClient tmr = do
lastActiveTimestamps <- catMaybes <$> lookupClientsLastActiveTimestamps brigClient tmr.id
if null lastActiveTimestamps
then do
pure $ TeamMember tmr.id tmr.legalhold Nothing
else do
let lastActive = maximum lastActiveTimestamps
pure $ TeamMember tmr.id tmr.legalhold (Just lastActive)

process :: TeamId -> ClientState -> ClientState -> IO [TeamMember]
process teamId brigClient galleyClient =
runConduit
$ selectTeamMembers galleyClient teamId
.| Conduit.concat
.| Conduit.mapM (lookUpActivity brigClient)
.| CL.consume

main :: IO ()
main = do
opts <- execParser (info (helper <*> optsParser) desc)
logger <- initLogger
brigClient <- initCas opts.brigDb logger
galleyClient <- initCas opts.galleyDb logger
teamMembers <- process opts.teamId brigClient galleyClient
for_ teamMembers $ \tm -> Log.info logger $ Log.msg (show tm)
where
initLogger =
Log.new
. Log.setLogLevel Log.Info
. Log.setOutput Log.StdOut
. Log.setFormat Nothing
. Log.setBufSize 0
$ Log.defSettings
initCas settings l =
C.init
. C.setLogger (C.mkLogger l)
. C.setContacts settings.host []
. C.setPortNumber (fromIntegral settings.port)
. C.setKeyspace settings.keyspace
. C.setProtocolVersion C.V4
$ C.defSettings
desc = header "team-info" <> progDesc "get team info" <> fullDesc
134 changes: 134 additions & 0 deletions tools/db/team-info/src/TeamInfo/Types.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TemplateHaskell #-}

-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2024 Wire Swiss GmbH <opensource@wire.com>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

module TeamInfo.Types where

import Cassandra as C
import Control.Lens
import Data.Id
import Data.LegalHold (UserLegalHoldStatus)
import Data.Text.Strict.Lens
import Data.Time
import Database.CQL.Protocol hiding (Result)
import Imports
import Options.Applicative

data CassandraSettings = CassandraSettings
{ host :: String,
port :: Int,
keyspace :: C.Keyspace
}

data Opts = Opts
{ brigDb :: CassandraSettings,
galleyDb :: CassandraSettings,
teamId :: TeamId
}

optsParser :: Parser Opts
optsParser =
Opts
<$> brigCassandraParser
<*> galleyCassandraParser
<*> ( option
auto
( long "team-id"
<> short 't'
<> metavar "ID"
<> help "Team ID"
)
)

galleyCassandraParser :: Parser CassandraSettings
galleyCassandraParser =
CassandraSettings
<$> strOption
( long "galley-cassandra-host"
<> metavar "HOST"
<> help "Cassandra Host for galley"
<> value "localhost"
<> showDefault
)
<*> option
auto
( long "galley-cassandra-port"
<> metavar "PORT"
<> help "Cassandra Port for galley"
<> value 9043
<> showDefault
)
<*> ( C.Keyspace
. view packed
<$> strOption
( long "galley-cassandra-keyspace"
<> metavar "STRING"
<> help "Cassandra Keyspace for galley"
<> value "galley_test"
<> showDefault
)
)

brigCassandraParser :: Parser CassandraSettings
brigCassandraParser =
CassandraSettings
<$> strOption
( long "brig-cassandra-host"
<> metavar "HOST"
<> help "Cassandra Host for brig"
<> value "localhost"
<> showDefault
)
<*> option
auto
( long "brig-cassandra-port"
<> metavar "PORT"
<> help "Cassandra Port for brig"
<> value 9042
<> showDefault
)
<*> ( C.Keyspace
. view packed
<$> strOption
( long "brig-cassandra-keyspace"
<> metavar "STRING"
<> help "Cassandra Keyspace for brig"
<> value "brig_test"
<> showDefault
)
)

data TeamMemberRow = TeamMemberRow
{ id :: UserId,
legalhold :: Maybe UserLegalHoldStatus
}
deriving (Show, Generic)

recordInstance ''TeamMemberRow

data TeamMember = TeamMember
{ id :: UserId,
legalhold :: Maybe UserLegalHoldStatus,
lastActive :: Maybe UTCTime
}
deriving (Generic)

-- output as csv
instance Show TeamMember where
show tm = show tm.id <> "," <> maybe " " show tm.legalhold <> "," <> maybe " " show tm.lastActive
Loading