module Test.Migration.MeetingLinks where
import API.Galley
import qualified Crypto.Hash as Crypto
import Crypto.Hash.Algorithms (SHA256 (SHA256))
import qualified Data.ByteArray as BA
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64.URL as B64URL
import qualified Data.ByteString.Char8 as BSC
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Data.Time.Clock
import qualified Data.UUID as UUID
import qualified Data.UUID.V4 as UUIDV4
import SetupHelpers
import Test.Meetings (defaultMeetingJson, getMeetingIdAndDomain, linkKeyAndCode, withLicensedBackendR)
import Testlib.PG (execSql)
import Testlib.Prelude
import Text.Regex.TDFA ((=~))
backfillSql :: String
backfillSql :: String
backfillSql =
String
"INSERT INTO conversation_codes (key, conversation, password, value, expires_at, target)\n\
\SELECT\n\
\ k.key,\n\
\ m.id,\n\
\ NULL,\n\
\ translate(encode(substring(decode(replace(gen_random_uuid()::text, '-', ''), 'hex') FROM 1 FOR 15), 'base64'), '+/', '-_'),\n\
\ now() + make_interval(secs => 2147483647),\n\
\ 'meeting'\n\
\FROM meetings m\n\
\CROSS JOIN LATERAL (\n\
\ SELECT translate(encode(substring(sha256(('meeting:' || m.id::text)::bytea) FROM 1 FOR 15), 'base64'), '+/', '-_') AS key\n\
\) k\n\
\WHERE NOT EXISTS (\n\
\ SELECT 1 FROM conversation_codes live\n\
\ WHERE live.key = k.key AND live.expires_at > now()\n\
\)\n\
\ON CONFLICT (key) DO UPDATE\n\
\SET conversation = EXCLUDED.conversation,\n\
\ password = EXCLUDED.password,\n\
\ value = EXCLUDED.value,\n\
\ expires_at = EXCLUDED.expires_at,\n\
\ target = EXCLUDED.target\n\
\WHERE conversation_codes.expires_at <= now();"
testMeetingJoinCodeBackfill :: (HasCallStack) => App ()
testMeetingJoinCodeBackfill :: HasCallStack => App ()
testMeetingJoinCodeBackfill = (BackendResource -> String -> App ()) -> App ()
forall a.
HasCallStack =>
(BackendResource -> String -> App a) -> App a
withLicensedBackendR ((BackendResource -> String -> App ()) -> App ())
-> (BackendResource -> String -> App ()) -> App ()
forall a b. (a -> b) -> a -> b
$ \BackendResource
backend String
dom -> do
(owner, _tid, [bob]) <- String -> Int -> App (Value, String, [Value])
forall domain.
(HasCallStack, MakesValue domain) =>
domain -> Int -> App (Value, String, [Value])
createTeam String
dom Int
2
now <- liftIO getCurrentTime
let newMeeting = String -> UTCTime -> UTCTime -> [String] -> Value
defaultMeetingJson String
"Backfill Meeting" (NominalDiffTime -> UTCTime -> UTCTime
addUTCTime NominalDiffTime
3600 UTCTime
now) (NominalDiffTime -> UTCTime -> UTCTime
addUTCTime NominalDiffTime
7200 UTCTime
now) []
meetingA <- postMeetings owner newMeeting >>= getJSON 201
(meetingAId, domain) <- getMeetingIdAndDomain meetingA
linkA <- meetingA %. "link" >>= asString
meetingBId <- T.unpack . UUID.toText <$> liftIO UUIDV4.nextRandom
creatorId <- owner %. "id" >>= asString
convAId <- meetingA %. "conversation" %. "qualified_id" %. "id" >>= asString
execSql backend
. T.pack
$ "INSERT INTO meetings (id, title, creator, start_time, end_time, tzid, mtype,"
<> " conversation_id, invited_emails, trial, created_at, updated_at)"
<> " VALUES ('"
<> meetingBId
<> "', 'Legacy Backfill Meeting', '"
<> creatorId
<> "',"
<> " now() + interval '1 hour', now() + interval '2 hours', 'Europe/Berlin', 'scheduled',"
<> " '"
<> convAId
<> "', '{}', false, now(), now())"
meetingCId <- T.unpack . UUID.toText <$> liftIO UUIDV4.nextRandom
execSql backend
. T.pack
$ "INSERT INTO meetings (id, title, creator, start_time, end_time, tzid, mtype,"
<> " conversation_id, invited_emails, trial, created_at, updated_at)"
<> " VALUES ('"
<> meetingCId
<> "', 'Expired Backfill Meeting', '"
<> creatorId
<> "',"
<> " now() + interval '1 hour', now() + interval '2 hours', 'Europe/Berlin', 'scheduled',"
<> " '"
<> convAId
<> "', '{}', false, now(), now())"
execSql backend
. T.pack
$ "INSERT INTO conversation_codes (key, conversation, password, value, expires_at, target)"
<> " VALUES ('"
<> meetingJoinCodeKey meetingCId
<> "', '"
<> convAId
<> "', NULL, 'stale-expired-value', now() - interval '1 hour', 'meeting')"
execSql backend (T.pack backfillSql)
linkB <- fetchLink owner meetingBId
let expected = String
"meeting-join/\\?key=[A-Za-z0-9_-]{20}&code=[A-Za-z0-9_-]{6,20}&domain=" String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
domain
assertBool ("unexpected backfilled meeting link: " <> linkB) (linkB =~ expected)
fetchedA <- getMeeting owner domain meetingAId >>= getJSON 200
fetchedLinkA <- fetchedA %. "link" >>= asString
assertBool "backfill must not touch an existing meeting's link" (fetchedLinkA == linkA)
let (keyB, codeB) = linkKeyAndCode linkB
getMeetingCodeCheck bob domain keyB codeB Nothing >>= assertStatus 200
postMeetingJoin bob domain keyB codeB Nothing >>= assertStatus 200
linkC <- fetchLink owner meetingCId
assertBool ("unexpected backfilled meeting link: " <> linkC) (linkC =~ expected)
let (keyC, codeC) = linkKeyAndCode linkC
assertBool "backfill must reuse the meeting's stable key" (keyC == meetingJoinCodeKey meetingCId)
assertBool "backfill must replace the expired code value" (codeC /= "stale-expired-value")
getMeetingCodeCheck bob domain keyC codeC Nothing >>= assertStatus 200
postMeetingJoin bob domain keyC codeC Nothing >>= assertStatus 200
execSql backend (T.pack backfillSql)
linkB' <- fetchLink owner meetingBId
assertBool "backfill must not rotate an existing meeting's link" (linkB' == linkB)
linkC' <- fetchLink owner meetingCId
assertBool "backfill must not rotate a replaced code" (linkC' == linkC)
fetchedA' <- getMeeting owner domain meetingAId >>= getJSON 200
fetchedLinkA' <- fetchedA' %. "link" >>= asString
assertBool "backfill must not touch an existing meeting's link" (fetchedLinkA' == linkA)
fetchLink :: (HasCallStack) => Value -> String -> App String
fetchLink :: HasCallStack => Value -> String -> App String
fetchLink Value
owner String
mid = do
listResp <- Value -> App Response
forall user.
(HasCallStack, MakesValue user) =>
user -> App Response
getMeetingsList Value
owner
assertSuccess listResp
meetings <- listResp.json & asList
ms <- findMeetingById mid meetings
case ms of
Maybe Value
Nothing -> String -> App String
forall a. HasCallStack => String -> App a
assertFailure (String
"meeting " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
mid String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
" missing from meetings list")
Just Value
m -> Value
m Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"link" App Value -> (Value -> App String) -> App String
forall a b. App a -> (a -> App b) -> App b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= Value -> App String
forall a. (HasCallStack, MakesValue a) => a -> App String
asString
findMeetingById :: (HasCallStack) => String -> [Value] -> App (Maybe Value)
findMeetingById :: HasCallStack => String -> [Value] -> App (Maybe Value)
findMeetingById String
mid [Value]
meetings = do
ids <- (Value -> App String) -> [Value] -> App [String]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
(a -> m b) -> t a -> m (t b)
forall (m :: * -> *) a b. Monad m => (a -> m b) -> [a] -> m [b]
mapM (\Value
m -> Value
m Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"qualified_id" App Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"id" App Value -> (Value -> App String) -> App String
forall a b. App a -> (a -> App b) -> App b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= Value -> App String
forall a. (HasCallStack, MakesValue a) => a -> App String
asString) [Value]
meetings
pure (fmap fst (find ((== mid) . snd) (zip meetings ids)))
meetingJoinCodeKey :: (HasCallStack) => String -> String
meetingJoinCodeKey :: HasCallStack => String -> String
meetingJoinCodeKey String
mid =
Text -> String
T.unpack
(Text -> String)
-> (Digest SHA256 -> Text) -> Digest SHA256 -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> Text
decodeUtf8
(ByteString -> Text)
-> (Digest SHA256 -> ByteString) -> Digest SHA256 -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> ByteString
B64URL.encode
(ByteString -> ByteString)
-> (Digest SHA256 -> ByteString) -> Digest SHA256 -> ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> ByteString -> ByteString
BS.take Int
15
(ByteString -> ByteString)
-> (Digest SHA256 -> ByteString) -> Digest SHA256 -> ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Digest SHA256 -> ByteString
forall bin bout.
(ByteArrayAccess bin, ByteArray bout) =>
bin -> bout
BA.convert
(Digest SHA256 -> String) -> Digest SHA256 -> String
forall a b. (a -> b) -> a -> b
$ SHA256 -> ByteString -> Digest SHA256
forall ba alg.
(ByteArrayAccess ba, HashAlgorithm alg) =>
alg -> ba -> Digest alg
Crypto.hashWith SHA256
SHA256 (ByteString -> ByteString -> ByteString
BSC.append (String -> ByteString
BSC.pack String
"meeting:") (Text -> ByteString
encodeUtf8 (String -> Text
T.pack String
mid)))