packages feed

pgmq-inspect-0.7.0.0: test/PrivilegesSpec.hs

module PrivilegesSpec (tests) where

import Control.Exception (bracket)
import Control.Monad (forM_)
import Data.Aeson (Value (..), eitherDecode)
import Data.Text (Text)
import Data.Text.IO qualified as T
import EphemeralDb
import EphemeralPg qualified as Pg
import Hasql.Connection qualified as Connection
import Hasql.Connection.Settings qualified as Settings
import Hasql.Decoders qualified as D
import Hasql.Pool qualified as Pool
import Hasql.Pool.Config qualified as PC
import Hasql.Session qualified as Session
import Hasql.Statement (unpreparable)
import HttpSpec (session)
import Network.WebSockets qualified as WS
import Pgmq.Effectful (PgmqRuntimeError (..))
import Pgmq.Inspect.Config
import Pgmq.Inspect.Env
import Pgmq.Inspect.Error (sqlState)
import RouterSpec (lookupKey)
import Test.Tasty
import Test.Tasty.HUnit
import TestServer

tests :: TestTree
tests =
  testGroup
    "Inspection role privileges"
    [ testCase "all reads and live snapshot work; consumption/mutation denied; future queues inherit SELECT" $ do
        result <- withPgmqDb $ \owner db -> do
          session owner (Session.script "SELECT pgmq.create('role_current'); SELECT pgmq.send('role_current','{}'); SELECT pgmq.send('role_current','{}'); SELECT pgmq.archive('role_current',1); SELECT pgmq.bind_topic('role.*','role_current'); SELECT pgmq.enable_notify_insert('role_current',250)")
          sql <- T.readFile "test/fixtures/inspection-role.sql"
          session owner (Session.script sql)
          session owner (Session.script "SELECT pgmq.create('role_future'); SELECT pgmq.send('role_future','{}')")
          let settings = Pg.connectionSettings db <> Settings.user "pgmq_inspector"
              poolConfig = PC.settings [PC.size 2, PC.acquisitionTimeout 1, PC.staticConnectionSettings settings]
          bracket (Pool.acquire poolConfig) Pool.release $ \pool ->
            withTestServer defaultInspectConfig (poolEnv pool (Just settings)) $ \port -> do
              forM_ ["/", "/openapi.json", "/queues", "/queues/role_current", "/queues/role_current/metrics", "/queues/role_current/messages", "/queues/role_current/messages/2", "/queues/role_current/archive", "/queues/role_current/archive/1", "/queues/role_current/bindings", "/metrics", "/bindings", "/routing/test?routing_key=role.created", "/notify/throttles", "/health/live", "/health/ready", "/queues/role_future/messages", "/queues/role_future/metrics"] $ \path -> do
                r <- get port path
                assertEqual (path <> " failed: " <> show (replyBody r)) 200 (replyStatus r)
              WS.runClient "127.0.0.1" port "/ws" $ \conn -> do
                WS.sendTextData conn ("{\"type\":\"subscribe\",\"queue\":\"role_current\"}" :: Text)
                bytes <- WS.receiveData conn
                row <- either assertFailure pure (eitherDecode bytes :: Either String Value)
                lookupKey "type" row @?= Just (String "snapshot")
          bracket (Connection.acquire settings >>= either (assertFailure . show) pure) Connection.release $ \conn -> do
            effective <- Connection.use conn (Session.statement () (unpreparable "SELECT current_setting('default_transaction_read_only'), current_setting('statement_timeout'), current_setting('lock_timeout')" mempty (D.singleRow ((,,) <$> D.column (D.nonNullable D.text) <*> D.column (D.nonNullable D.text) <*> D.column (D.nonNullable D.text))))) >>= either (assertFailure . show) pure
            effective @?= ("on", "2s", "250ms")
            -- Turn off the default transaction safeguard to prove the ACL itself
            -- denies mutation, including sequence advancement.
            Connection.use conn (Session.script "SET default_transaction_read_only=off") >>= either (assertFailure . show) pure
            forM_ ["SELECT pgmq.read('role_current',0,1)", "SELECT pgmq.pop('role_current')", "SELECT pgmq.send('role_current','{}')", "SELECT pgmq.archive('role_current',2)", "SELECT pgmq.delete('role_current',2)", "SELECT pgmq.purge_queue('role_current')", "SELECT pgmq.drop_queue('role_current')", "SELECT pgmq.create('role_forbidden')", "UPDATE pgmq.q_role_current SET read_ct=read_ct+1", "SELECT nextval('pgmq.q_role_current_msg_id_seq')"] $ \sql' -> do
              answer <- Connection.use conn (Session.script sql')
              case answer of
                Left err -> assertEqual ("wrong failure for " <> show sql') (Just "42501") (sqlState (PgmqSessionError err))
                Right () -> assertFailure ("write unexpectedly permitted: " <> show sql')
        either (assertFailure . show) pure result
    ]