added: cli option --dump-config prints loaded config and exits
This is most useful for automated tests for upcoming configuration features. Can also be used for debugging.
This commit is contained in:
committed by
Wolfgang Walther
parent
d218a9eaff
commit
eb46cf2662
+9
-5
@@ -27,8 +27,9 @@ import Network.Wai.Handler.Warp (defaultSettings, runSettings,
|
|||||||
import System.IO (BufferMode (..), hSetBuffering)
|
import System.IO (BufferMode (..), hSetBuffering)
|
||||||
|
|
||||||
import PostgREST.App (postgrest)
|
import PostgREST.App (postgrest)
|
||||||
import PostgREST.Config (AppConfig (..), configPoolTimeout',
|
import PostgREST.Config (AppConfig (..), CLI (..), Command (..),
|
||||||
prettyVersion, readPathShowHelp,
|
configPoolTimeout', dumpAppConfig,
|
||||||
|
prettyVersion, readCLIShowHelp,
|
||||||
readValidateConfig)
|
readValidateConfig)
|
||||||
import PostgREST.DbStructure (getDbStructure, getPgVersion)
|
import PostgREST.DbStructure (getDbStructure, getPgVersion)
|
||||||
import PostgREST.Error (PgError (PgError), checkIsFatal,
|
import PostgREST.Error (PgError (PgError), checkIsFatal,
|
||||||
@@ -57,10 +58,13 @@ main = do
|
|||||||
hSetBuffering stderr NoBuffering
|
hSetBuffering stderr NoBuffering
|
||||||
|
|
||||||
-- read path from commad line
|
-- read path from commad line
|
||||||
path <- readPathShowHelp
|
opts <- readCLIShowHelp
|
||||||
|
|
||||||
-- build the 'AppConfig' from the config file path
|
-- build the 'AppConfig' from the config file path
|
||||||
conf <- readValidateConfig path
|
conf <- readValidateConfig $ cliPath opts
|
||||||
|
|
||||||
|
-- dump config and exit if option is set
|
||||||
|
when (cliCommand opts == CmdDumpConfig) $ dumpAppConfig conf
|
||||||
|
|
||||||
-- These are config values that can't be reloaded at runtime. Reloading some of them would imply restarting the web server.
|
-- These are config values that can't be reloaded at runtime. Reloading some of them would imply restarting the web server.
|
||||||
let
|
let
|
||||||
@@ -122,7 +126,7 @@ main = do
|
|||||||
|
|
||||||
-- Re-read the config on SIGUSR2
|
-- Re-read the config on SIGUSR2
|
||||||
void $ installHandler sigUSR2 (
|
void $ installHandler sigUSR2 (
|
||||||
Catch $ reReadConfig path refConf
|
Catch $ reReadConfig (cliPath opts) refConf
|
||||||
) Nothing
|
) Nothing
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
+97
-25
@@ -19,9 +19,12 @@ Other hardcoded options such as the minimum version number also belong here.
|
|||||||
|
|
||||||
module PostgREST.Config ( prettyVersion
|
module PostgREST.Config ( prettyVersion
|
||||||
, docsVersion
|
, docsVersion
|
||||||
|
, CLI (..)
|
||||||
|
, Command (..)
|
||||||
, AppConfig (..)
|
, AppConfig (..)
|
||||||
, configPoolTimeout'
|
, configPoolTimeout'
|
||||||
, readPathShowHelp
|
, dumpAppConfig
|
||||||
|
, readCLIShowHelp
|
||||||
, readValidateConfig
|
, readValidateConfig
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
@@ -30,24 +33,25 @@ import qualified Data.ByteString as B
|
|||||||
import qualified Data.ByteString.Base64 as B64
|
import qualified Data.ByteString.Base64 as B64
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import qualified Data.Configurator as C
|
import qualified Data.Configurator as C
|
||||||
import Data.Either.Combinators (whenLeft)
|
|
||||||
import qualified Text.PrettyPrint.ANSI.Leijen as L
|
import qualified Text.PrettyPrint.ANSI.Leijen as L
|
||||||
|
|
||||||
import Control.Lens (preview)
|
import Control.Lens (preview)
|
||||||
import Control.Monad (fail)
|
import Control.Monad (fail)
|
||||||
import Crypto.JWT (JWKSet, StringOrURI, stringOrUri)
|
import Crypto.JWT (JWKSet, StringOrURI, stringOrUri)
|
||||||
import Data.List.NonEmpty (fromList)
|
import Data.Aeson (encode, toJSON)
|
||||||
import Data.Scientific (floatingOrInteger)
|
import Data.Either.Combinators (fromRight', whenLeft)
|
||||||
import Data.Text (dropEnd, dropWhileEnd, intercalate, pack,
|
import Data.List.NonEmpty (fromList, toList)
|
||||||
replace, splitOn, strip, stripPrefix, take,
|
import Data.Scientific (floatingOrInteger)
|
||||||
unpack)
|
import Data.Text (dropEnd, dropWhileEnd, intercalate,
|
||||||
import Data.Text.IO (hPutStrLn)
|
pack, replace, splitOn, strip,
|
||||||
import Data.Version (versionBranch)
|
stripPrefix, take, toLower, unpack)
|
||||||
import Development.GitRev (gitHash)
|
import Data.Text.IO (hPutStrLn)
|
||||||
import Numeric (readOct)
|
import Data.Version (versionBranch)
|
||||||
import Paths_postgrest (version)
|
import Development.GitRev (gitHash)
|
||||||
import System.IO.Error (IOError)
|
import Numeric (readOct, showOct)
|
||||||
import System.Posix.Types (FileMode)
|
import Paths_postgrest (version)
|
||||||
|
import System.IO.Error (IOError)
|
||||||
|
import System.Posix.Types (FileMode)
|
||||||
|
|
||||||
import Control.Applicative
|
import Control.Applicative
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
@@ -62,9 +66,15 @@ import PostgREST.Types (JSPath, JSPathExp (..),
|
|||||||
LogLevel (..))
|
LogLevel (..))
|
||||||
import Protolude hiding (concat, hPutStrLn,
|
import Protolude hiding (concat, hPutStrLn,
|
||||||
intercalate, null, replace, take,
|
intercalate, null, replace, take,
|
||||||
toS, (<>))
|
toList, toLower, toS, (<>))
|
||||||
import Protolude.Conv (toS)
|
import Protolude.Conv (toS)
|
||||||
|
|
||||||
|
-- | Command line interface options
|
||||||
|
data CLI = CLI
|
||||||
|
{ cliCommand :: Command
|
||||||
|
, cliPath :: FilePath }
|
||||||
|
|
||||||
|
data Command = CmdRun | CmdDumpConfig deriving (Eq)
|
||||||
|
|
||||||
-- | Config file settings for the server
|
-- | Config file settings for the server
|
||||||
data AppConfig = AppConfig {
|
data AppConfig = AppConfig {
|
||||||
@@ -103,6 +113,7 @@ data AppConfig = AppConfig {
|
|||||||
|
|
||||||
, configDbPrepared :: Bool
|
, configDbPrepared :: Bool
|
||||||
}
|
}
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
configPoolTimeout' :: (Fractional a) => AppConfig -> a
|
configPoolTimeout' :: (Fractional a) => AppConfig -> a
|
||||||
configPoolTimeout' =
|
configPoolTimeout' =
|
||||||
@@ -122,13 +133,13 @@ prettyVersion =
|
|||||||
docsVersion :: Text
|
docsVersion :: Text
|
||||||
docsVersion = "v" <> dropEnd 1 (dropWhileEnd (/= '.') prettyVersion)
|
docsVersion = "v" <> dropEnd 1 (dropWhileEnd (/= '.') prettyVersion)
|
||||||
|
|
||||||
-- | Read config the file path from the command line. Also prints help.
|
-- | Read command line interface options. Also prints help.
|
||||||
readPathShowHelp :: IO FilePath
|
readCLIShowHelp :: IO CLI
|
||||||
readPathShowHelp = customExecParser parserPrefs opts
|
readCLIShowHelp = customExecParser parserPrefs opts
|
||||||
where
|
where
|
||||||
parserPrefs = prefs showHelpOnError
|
parserPrefs = prefs showHelpOnError
|
||||||
|
|
||||||
opts = info (helper <*> pathParser) $
|
opts = info (helper <*> cliParser) $
|
||||||
fullDesc
|
fullDesc
|
||||||
<> progDesc (
|
<> progDesc (
|
||||||
"PostgREST "
|
"PostgREST "
|
||||||
@@ -140,11 +151,16 @@ readPathShowHelp = customExecParser parserPrefs opts
|
|||||||
L.<> nest 2 (hardline L.<> exampleCfg)
|
L.<> nest 2 (hardline L.<> exampleCfg)
|
||||||
)
|
)
|
||||||
|
|
||||||
pathParser :: Parser FilePath
|
cliParser :: Parser CLI
|
||||||
pathParser =
|
cliParser = CLI <$>
|
||||||
strArgument $
|
flag CmdRun CmdDumpConfig (
|
||||||
|
long "dump-config" <>
|
||||||
|
help "Dump loaded configuration and exit"
|
||||||
|
) <*>
|
||||||
|
strArgument (
|
||||||
metavar "FILENAME" <>
|
metavar "FILENAME" <>
|
||||||
help "Path to configuration file"
|
help "Path to configuration file"
|
||||||
|
)
|
||||||
|
|
||||||
exampleCfg :: Doc
|
exampleCfg :: Doc
|
||||||
exampleCfg = vsep . map (text . toS) . lines $
|
exampleCfg = vsep . map (text . toS) . lines $
|
||||||
@@ -225,6 +241,62 @@ readPathShowHelp = customExecParser parserPrefs opts
|
|||||||
|log-level = "error"
|
|log-level = "error"
|
||||||
|]
|
|]
|
||||||
|
|
||||||
|
-- | Dump the config
|
||||||
|
dumpAppConfig :: AppConfig -> IO ()
|
||||||
|
dumpAppConfig conf = do
|
||||||
|
putStr dump
|
||||||
|
exitSuccess
|
||||||
|
|
||||||
|
where
|
||||||
|
dump = unlines $ (\(k, v) -> k <> " = " <> v) <$>
|
||||||
|
pgrstSettings ++ appSettings
|
||||||
|
|
||||||
|
-- apply conf to all pgrst settings
|
||||||
|
pgrstSettings = (\(k, v) -> (k, v conf)) <$>
|
||||||
|
[("db-uri", q . configDbUri)
|
||||||
|
,("db-schema", q . intercalate "," . toList . configSchemas)
|
||||||
|
,("db-anon-role", q . configAnonRole)
|
||||||
|
,("db-pool", show . configPoolSize)
|
||||||
|
,("db-pool-timeout", show . configPoolTimeout)
|
||||||
|
,("db-extra-search-path", q . intercalate "," . configExtraSearchPath)
|
||||||
|
,("db-channel", q . configDbChannel)
|
||||||
|
,("db-channel-enabled", toLower . show . configDbChannelEnabled)
|
||||||
|
,("db-tx-end", q . showTxEnd)
|
||||||
|
,("db-prepared-statements", toLower . show . configDbPrepared)
|
||||||
|
,("server-host", q . configHost)
|
||||||
|
,("server-port", show . configPort)
|
||||||
|
,("server-unix-socket", q . maybe mempty pack . configSocket)
|
||||||
|
,("server-unix-socket-mode", q . pack . showSocketMode)
|
||||||
|
,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenAPIProxyUri)
|
||||||
|
,("jwt-secret", q . toS . showJwtSecret)
|
||||||
|
,("jwt-aud", toS . encode . maybe "" toJSON . configJwtAudience)
|
||||||
|
,("secret-is-base64", toLower . show . configJwtSecretIsBase64)
|
||||||
|
,("role-claim-key", q . intercalate mempty . fmap show . fromRight' . configRoleClaimKey)
|
||||||
|
,("max-rows", maybe "\"\"" show . configMaxRows)
|
||||||
|
,("pre-request", q . fromMaybe mempty . configPreReq)
|
||||||
|
,("root-spec", q . fromMaybe mempty . configRootSpec)
|
||||||
|
,("raw-media-types", q . toS . B.intercalate "," . configRawMediaTypes)
|
||||||
|
,("log-level", q . show . configLogLevel)
|
||||||
|
]
|
||||||
|
|
||||||
|
-- quote all app.settings
|
||||||
|
appSettings = second q <$> configSettings conf
|
||||||
|
|
||||||
|
-- quote strings and replace " with \"
|
||||||
|
q s = "\"" <> replace "\"" "\\\"" s <> "\""
|
||||||
|
|
||||||
|
showTxEnd c = case (configTxRollbackAll c, configTxAllowOverride c) of
|
||||||
|
( False, False ) -> "commit"
|
||||||
|
( False, True ) -> "commit-allow-override"
|
||||||
|
( True , False ) -> "rollback"
|
||||||
|
( True , True ) -> "rollback-allow-override"
|
||||||
|
showSocketMode c = showOct (fromRight' $ configSocketMode c) ""
|
||||||
|
showJwtSecret c
|
||||||
|
| configJwtSecretIsBase64 c = B64.encode secret
|
||||||
|
| otherwise = toS secret
|
||||||
|
where
|
||||||
|
secret = fromMaybe mempty $ configJwtSecret c
|
||||||
|
|
||||||
-- | Parse the config file
|
-- | Parse the config file
|
||||||
readAppConfig :: FilePath -> IO AppConfig
|
readAppConfig :: FilePath -> IO AppConfig
|
||||||
readAppConfig cfgPath = do
|
readAppConfig cfgPath = do
|
||||||
|
|||||||
+13
-2
@@ -546,7 +546,12 @@ sourceCTEName = "pgrst_source"
|
|||||||
-- | full jspath, e.g. .property[0].attr.detail
|
-- | full jspath, e.g. .property[0].attr.detail
|
||||||
type JSPath = [JSPathExp]
|
type JSPath = [JSPathExp]
|
||||||
-- | jspath expression, e.g. .property, .property[0] or ."property-dash"
|
-- | jspath expression, e.g. .property, .property[0] or ."property-dash"
|
||||||
data JSPathExp = JSPKey Text | JSPIdx Int deriving (Eq, Show)
|
data JSPathExp = JSPKey Text | JSPIdx Int deriving (Eq)
|
||||||
|
|
||||||
|
instance Show JSPathExp where
|
||||||
|
-- TODO: this needs to be quoted properly for special chars
|
||||||
|
show (JSPKey k) = "." <> show k
|
||||||
|
show (JSPIdx i) = "[" <> show i <> "]"
|
||||||
|
|
||||||
-- | Current database connection status data ConnectionStatus
|
-- | Current database connection status data ConnectionStatus
|
||||||
data ConnectionStatus
|
data ConnectionStatus
|
||||||
@@ -555,4 +560,10 @@ data ConnectionStatus
|
|||||||
| FatalConnectionError Text
|
| FatalConnectionError Text
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
data LogLevel = LogCrit | LogError | LogWarn | LogInfo deriving (Eq, Show)
|
data LogLevel = LogCrit | LogError | LogWarn | LogInfo deriving (Eq)
|
||||||
|
|
||||||
|
instance Show LogLevel where
|
||||||
|
show LogCrit = "crit"
|
||||||
|
show LogError = "error"
|
||||||
|
show LogWarn = "warn"
|
||||||
|
show LogInfo = "info"
|
||||||
|
|||||||
@@ -47,6 +47,54 @@ ok(){ result 'ok' "- $1"; }
|
|||||||
ko(){ result 'not ok' "- $1"; failedTests=$(( $failedTests + 1 )); }
|
ko(){ result 'not ok' "- $1"; failedTests=$(( $failedTests + 1 )); }
|
||||||
comment(){ echo "# $1"; }
|
comment(){ echo "# $1"; }
|
||||||
|
|
||||||
|
########################
|
||||||
|
# SYNCHRONOUS IO TESTS #
|
||||||
|
########################
|
||||||
|
|
||||||
|
dumpedConfigMatchesExpectation(){
|
||||||
|
# This test compares the dumped config vs. the corresponding file in ./configs/expected.
|
||||||
|
# To be used to test default values, config aliases and environment variables.
|
||||||
|
dump="$(mktemp)"
|
||||||
|
tap(){
|
||||||
|
if test $1 -eq 0; then
|
||||||
|
ok "dump of config file $2 does match expectation"
|
||||||
|
else
|
||||||
|
ko "dump of config file $2 does not match expectation"
|
||||||
|
fi
|
||||||
|
rm -f "$dump"
|
||||||
|
}
|
||||||
|
trap 'tap $? $1; trap - RETURN; return 0' ERR RETURN
|
||||||
|
postgrest --dump-config "$1" > "$dump"
|
||||||
|
diff "$dump" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
dumpedConfigIsValid(){
|
||||||
|
# This test compares the dumped config vs. the dumped-reread-redumped config.
|
||||||
|
# Re-reading the dumped config tests the validity of the config format.
|
||||||
|
# Re-dumping this config should yield no difference to the first dump, showing
|
||||||
|
# that the semantics have not changed by dumping.
|
||||||
|
# Note: only dump vs redump must be equal, the original config file can be different,
|
||||||
|
# because of default values, whitespace, and quoting
|
||||||
|
dump="$(mktemp)"
|
||||||
|
redump="$(mktemp)"
|
||||||
|
tap(){
|
||||||
|
if test $1 -eq 0; then
|
||||||
|
ok "dump of config file $2 is valid"
|
||||||
|
else
|
||||||
|
ko "dump of config file $2 is invalid"
|
||||||
|
fi
|
||||||
|
rm -f "$dump" "$redump"
|
||||||
|
}
|
||||||
|
trap 'tap $? $1; trap - RETURN; return 0' ERR RETURN
|
||||||
|
postgrest --dump-config "$1" > "$dump"
|
||||||
|
postgrest --dump-config "$dump" > "$redump"
|
||||||
|
diff "$dump" "$redump"
|
||||||
|
}
|
||||||
|
|
||||||
|
####################
|
||||||
|
# BACKGROUND TESTS #
|
||||||
|
####################
|
||||||
|
|
||||||
# Utilities to start/stop test PostgREST server running in the background
|
# Utilities to start/stop test PostgREST server running in the background
|
||||||
pgrStart(){
|
pgrStart(){
|
||||||
# stderr is not piped to /dev/null to catch errors on startup.
|
# stderr is not piped to /dev/null to catch errors on startup.
|
||||||
@@ -309,6 +357,23 @@ psql -l "$POSTGREST_TEST_CONNECTION" 1>/dev/null 2>/dev/null || bailOut 'postgre
|
|||||||
|
|
||||||
echo "Running IO tests.."
|
echo "Running IO tests.."
|
||||||
|
|
||||||
|
# run dumpConfigIsValid with as many inputs as possible
|
||||||
|
for cfg in configs/*.config
|
||||||
|
do
|
||||||
|
# ROLE_CLAIM_KEY is only used in one of the config files
|
||||||
|
# using a complex example here, to make sure the quoting works
|
||||||
|
ROLE_CLAIM_KEY='."https://www.example.com/roles"[0].value' \
|
||||||
|
dumpedConfigIsValid "$cfg" \
|
||||||
|
<<< "Y29ubmVjdGlvbl9zdHJpbmc=" # /dev/stdin is read by some config files, one of them expects Base64
|
||||||
|
done
|
||||||
|
|
||||||
|
# run dumpConfigMatchesExpectation with all expectations
|
||||||
|
for exp in configs/expected/*.config
|
||||||
|
do
|
||||||
|
cfg="$(sed -e 's|expected/||' <(echo $exp))"
|
||||||
|
dumpedConfigMatchesExpectation "$cfg" "$exp"
|
||||||
|
done
|
||||||
|
|
||||||
socketConnection
|
socketConnection
|
||||||
|
|
||||||
readSecretFromFile word.noeol 'simple (no EOL)'
|
readSecretFromFile word.noeol 'simple (no EOL)'
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
db-uri = "required"
|
||||||
|
db-schema = "required"
|
||||||
|
db-anon-role = "required"
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
db-uri = "required"
|
||||||
|
db-schema = "required"
|
||||||
|
db-anon-role = "required"
|
||||||
|
db-pool = 10
|
||||||
|
db-pool-timeout = 10
|
||||||
|
db-extra-search-path = "public"
|
||||||
|
db-channel = "pgrst"
|
||||||
|
db-channel-enabled = false
|
||||||
|
db-tx-end = "commit"
|
||||||
|
db-prepared-statements = true
|
||||||
|
server-host = "!4"
|
||||||
|
server-port = 3000
|
||||||
|
server-unix-socket = ""
|
||||||
|
server-unix-socket-mode = "660"
|
||||||
|
openapi-server-proxy-uri = ""
|
||||||
|
jwt-secret = ""
|
||||||
|
jwt-aud = ""
|
||||||
|
secret-is-base64 = false
|
||||||
|
role-claim-key = ".\"role\""
|
||||||
|
max-rows = ""
|
||||||
|
pre-request = ""
|
||||||
|
root-spec = ""
|
||||||
|
raw-media-types = ""
|
||||||
|
log-level = "error"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
db-uri = "tmp_db"
|
||||||
|
db-schema = "multi,tenant,setup"
|
||||||
|
db-anon-role = "root"
|
||||||
|
db-pool = 1
|
||||||
|
db-pool-timeout = 100
|
||||||
|
db-extra-search-path = "public,test"
|
||||||
|
db-channel = "postgrest"
|
||||||
|
db-channel-enabled = true
|
||||||
|
db-tx-end = "rollback-allow-override"
|
||||||
|
db-prepared-statements = false
|
||||||
|
server-host = "0.0.0.0"
|
||||||
|
server-port = 80
|
||||||
|
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
||||||
|
server-unix-socket-mode = "777"
|
||||||
|
openapi-server-proxy-uri = "https://postgrest.org"
|
||||||
|
jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5"
|
||||||
|
jwt-aud = "https://postgrest.org"
|
||||||
|
secret-is-base64 = true
|
||||||
|
role-claim-key = ".\"user\"[0].\"real-role\""
|
||||||
|
max-rows = 1000
|
||||||
|
pre-request = "please_run_fast"
|
||||||
|
root-spec = "openapi_v3"
|
||||||
|
raw-media-types = "application/vnd.pgrst.config"
|
||||||
|
log-level = "info"
|
||||||
|
app.settings.test = "test"
|
||||||
|
app.settings.test2 = "test"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
db-uri = "tmp_db"
|
||||||
|
db-schema = "multi, tenant,setup"
|
||||||
|
db-anon-role = "root"
|
||||||
|
db-pool = 1
|
||||||
|
db-pool-timeout = 100
|
||||||
|
db-extra-search-path = "public, test"
|
||||||
|
db-channel = "postgrest"
|
||||||
|
db-channel-enabled = true
|
||||||
|
db-tx-end = "rollback-allow-override"
|
||||||
|
db-prepared-statements = false
|
||||||
|
server-host = "0.0.0.0"
|
||||||
|
server-port = 80
|
||||||
|
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
||||||
|
server-unix-socket-mode = "777"
|
||||||
|
openapi-server-proxy-uri = "https://postgrest.org"
|
||||||
|
jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5"
|
||||||
|
jwt-aud = "https://postgrest.org"
|
||||||
|
secret-is-base64 = true
|
||||||
|
role-claim-key = ".user[0].\"real-role\""
|
||||||
|
max-rows = 1000
|
||||||
|
pre-request = "please_run_fast"
|
||||||
|
root-spec = "openapi_v3"
|
||||||
|
raw-media-types = "application/vnd.pgrst.config"
|
||||||
|
log-level = "info"
|
||||||
|
app.settings.test = "test"
|
||||||
|
app.settings.test2 = "test"
|
||||||
Reference in New Issue
Block a user