2018-09-27 14:22:49 +03:00
|
|
|
{-# LANGUAGE FlexibleContexts #-}
|
|
|
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2018-06-27 16:11:32 +03:00
|
|
|
|
|
|
|
module Main where
|
|
|
|
|
|
|
|
import Ops
|
|
|
|
|
2018-09-05 14:26:46 +03:00
|
|
|
import Control.Monad.STM (atomically)
|
2018-06-27 16:11:32 +03:00
|
|
|
import Data.Time.Clock (getCurrentTime)
|
|
|
|
import Options.Applicative
|
2018-07-06 08:13:46 +03:00
|
|
|
import System.Environment (lookupEnv)
|
2018-06-27 16:11:32 +03:00
|
|
|
import System.Exit (exitFailure)
|
|
|
|
|
2018-07-27 12:34:50 +03:00
|
|
|
import qualified Control.Concurrent as C
|
2018-06-27 16:11:32 +03:00
|
|
|
import qualified Data.Aeson as A
|
|
|
|
import qualified Data.ByteString.Char8 as BC
|
|
|
|
import qualified Data.ByteString.Lazy as BL
|
|
|
|
import qualified Data.ByteString.Lazy.Char8 as BLC
|
|
|
|
import qualified Data.Text as T
|
|
|
|
import qualified Data.Yaml as Y
|
2018-07-27 12:34:50 +03:00
|
|
|
import qualified Network.HTTP.Client as HTTP
|
|
|
|
import qualified Network.HTTP.Client.TLS as HTTP
|
|
|
|
import qualified Network.Wai.Handler.Warp as Warp
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2018-09-05 14:26:46 +03:00
|
|
|
import Hasura.Events.Lib
|
2018-10-25 21:16:25 +03:00
|
|
|
import Hasura.Logging (defaultLoggerSettings, mkLoggerCtx)
|
2018-06-27 16:11:32 +03:00
|
|
|
import Hasura.Prelude
|
|
|
|
import Hasura.RQL.DDL.Metadata (fetchMetadata)
|
2018-10-25 21:16:25 +03:00
|
|
|
import Hasura.RQL.Types (RoleName (..))
|
2018-07-20 10:22:46 +03:00
|
|
|
import Hasura.Server.App (mkWaiApp)
|
2018-09-27 14:22:49 +03:00
|
|
|
import Hasura.Server.Auth
|
2018-07-27 12:34:50 +03:00
|
|
|
import Hasura.Server.CheckUpdates (checkForUpdates)
|
2018-06-27 16:11:32 +03:00
|
|
|
import Hasura.Server.Init
|
|
|
|
|
|
|
|
import qualified Database.PG.Query as Q
|
2018-09-05 14:26:46 +03:00
|
|
|
import qualified Network.HTTP.Client.TLS as TLS
|
|
|
|
import qualified Network.Wreq.Session as WrqS
|
2018-06-27 16:11:32 +03:00
|
|
|
|
|
|
|
data RavenOptions
|
|
|
|
= RavenOptions
|
|
|
|
{ roConnInfo :: !RawConnInfo
|
|
|
|
, roMode :: !RavenMode
|
|
|
|
} deriving (Show, Eq)
|
|
|
|
|
|
|
|
data ServeOptions
|
|
|
|
= ServeOptions
|
2018-06-29 14:05:09 +03:00
|
|
|
{ soPort :: !Int
|
|
|
|
, soConnParams :: !Q.ConnParams
|
|
|
|
, soTxIso :: !Q.TxIsolation
|
|
|
|
, soRootDir :: !(Maybe String)
|
|
|
|
, soAccessKey :: !(Maybe AccessKey)
|
add support for jwt authorization (close #186) (#255)
The API:
1. HGE has `--jwt-secret` flag or `HASURA_GRAPHQL_JWT_SECRET` env var. The value of which is a JSON.
2. The structure of this JSON is: `{"type": "<standard-JWT-algorithms>", "key": "<the-key>"}`
`type` : Standard JWT algos : `HS256`, `RS256`, `RS512` etc. (see jwt.io).
`key`:
i. Incase of symmetric key, the key as it is.
ii. Incase of asymmetric keys, only the public key, in a PEM encoded string or as a X509 certificate.
3. The claims in the JWT token must contain the following:
i. `x-hasura-default-role` field: default role of that user
ii. `x-hasura-allowed-roles` : A list of allowed roles for the user. The default role is overriden by `x-hasura-role` header.
4. The claims in the JWT token, can have other `x-hasura-*` fields where their values can only be strings.
5. The JWT tokens are sent as `Authorization: Bearer <token>` headers.
---
To test:
1. Generate a shared secret (for HMAC-SHA256) or RSA key pair.
2. Goto https://jwt.io/ , add the keys
3. Edit the claims to have `x-hasura-role` (mandatory) and other `x-hasura-*` fields. Add permissions related to the claims to test permissions.
4. Start HGE with `--jwt-secret` flag or `HASURA_GRAPHQL_JWT_SECRET` env var, which takes a JSON string: `{"type": "HS256", "key": "mylongsharedsecret"}` or `{"type":"RS256", "key": "<PEM-encoded-public-key>"}`
5. Copy the JWT token from jwt.io and use it in the `Authorization: Bearer <token>` header.
---
TODO: Support EC public keys. It is blocked on frasertweedale/hs-jose#61
2018-08-30 13:32:09 +03:00
|
|
|
, soWebHook :: !(Maybe Webhook)
|
|
|
|
, soJwtSecret :: !(Maybe Text)
|
2018-10-25 21:16:25 +03:00
|
|
|
, soUnAuthRole :: !(Maybe RoleName)
|
2018-09-27 14:22:49 +03:00
|
|
|
, soCorsConfig :: !CorsConfigFlags
|
2018-06-29 14:05:09 +03:00
|
|
|
, soEnableConsole :: !Bool
|
2018-06-27 16:11:32 +03:00
|
|
|
} deriving (Show, Eq)
|
|
|
|
|
|
|
|
data RavenMode
|
|
|
|
= ROServe !ServeOptions
|
|
|
|
| ROExport
|
|
|
|
| ROClean
|
|
|
|
| ROExecute
|
|
|
|
deriving (Show, Eq)
|
|
|
|
|
|
|
|
parseRavenMode :: Parser RavenMode
|
|
|
|
parseRavenMode = subparser
|
|
|
|
( command "serve" (info (helper <*> serveOptsParser)
|
|
|
|
( progDesc "Start the HTTP api server" ))
|
|
|
|
<> command "export" (info (pure ROExport)
|
2018-07-11 11:24:13 +03:00
|
|
|
( progDesc "Export graphql-engine's schema to stdout" ))
|
2018-06-27 16:11:32 +03:00
|
|
|
<> command "clean" (info (pure ROClean)
|
2018-07-11 11:24:13 +03:00
|
|
|
( progDesc "Clean graphql-engine's metadata to start afresh" ))
|
2018-06-27 16:11:32 +03:00
|
|
|
<> command "execute" (info (pure ROExecute)
|
|
|
|
( progDesc "Execute a query" ))
|
|
|
|
)
|
|
|
|
where
|
|
|
|
serveOptsParser = ROServe <$> serveOpts
|
|
|
|
serveOpts = ServeOptions
|
|
|
|
<$> parseServerPort
|
|
|
|
<*> parseConnParams
|
|
|
|
<*> parseTxIsolation
|
|
|
|
<*> parseRootDir
|
|
|
|
<*> parseAccessKey
|
|
|
|
<*> parseWebHook
|
add support for jwt authorization (close #186) (#255)
The API:
1. HGE has `--jwt-secret` flag or `HASURA_GRAPHQL_JWT_SECRET` env var. The value of which is a JSON.
2. The structure of this JSON is: `{"type": "<standard-JWT-algorithms>", "key": "<the-key>"}`
`type` : Standard JWT algos : `HS256`, `RS256`, `RS512` etc. (see jwt.io).
`key`:
i. Incase of symmetric key, the key as it is.
ii. Incase of asymmetric keys, only the public key, in a PEM encoded string or as a X509 certificate.
3. The claims in the JWT token must contain the following:
i. `x-hasura-default-role` field: default role of that user
ii. `x-hasura-allowed-roles` : A list of allowed roles for the user. The default role is overriden by `x-hasura-role` header.
4. The claims in the JWT token, can have other `x-hasura-*` fields where their values can only be strings.
5. The JWT tokens are sent as `Authorization: Bearer <token>` headers.
---
To test:
1. Generate a shared secret (for HMAC-SHA256) or RSA key pair.
2. Goto https://jwt.io/ , add the keys
3. Edit the claims to have `x-hasura-role` (mandatory) and other `x-hasura-*` fields. Add permissions related to the claims to test permissions.
4. Start HGE with `--jwt-secret` flag or `HASURA_GRAPHQL_JWT_SECRET` env var, which takes a JSON string: `{"type": "HS256", "key": "mylongsharedsecret"}` or `{"type":"RS256", "key": "<PEM-encoded-public-key>"}`
5. Copy the JWT token from jwt.io and use it in the `Authorization: Bearer <token>` header.
---
TODO: Support EC public keys. It is blocked on frasertweedale/hs-jose#61
2018-08-30 13:32:09 +03:00
|
|
|
<*> parseJwtSecret
|
2018-10-25 21:16:25 +03:00
|
|
|
<*> parseUnAuthRole
|
2018-09-27 14:22:49 +03:00
|
|
|
<*> parseCorsConfig
|
2018-06-29 14:05:09 +03:00
|
|
|
<*> parseEnableConsole
|
2018-06-27 16:11:32 +03:00
|
|
|
|
|
|
|
parseArgs :: IO RavenOptions
|
|
|
|
parseArgs = execParser opts
|
|
|
|
where
|
|
|
|
optParser = RavenOptions <$> parseRawConnInfo <*> parseRavenMode
|
|
|
|
opts = info (helper <*> optParser)
|
|
|
|
( fullDesc <>
|
2018-07-11 11:24:13 +03:00
|
|
|
header "Hasura's graphql-engine - Exposes Postgres over GraphQL")
|
2018-06-27 16:11:32 +03:00
|
|
|
|
|
|
|
printJSON :: (A.ToJSON a) => a -> IO ()
|
|
|
|
printJSON = BLC.putStrLn . A.encode
|
|
|
|
|
|
|
|
printYaml :: (A.ToJSON a) => a -> IO ()
|
|
|
|
printYaml = BC.putStrLn . Y.encode
|
|
|
|
|
2018-10-10 09:46:38 +03:00
|
|
|
getEnableConsoleEnv :: IO Bool
|
|
|
|
getEnableConsoleEnv = do
|
|
|
|
mVal <- fmap T.pack <$> lookupEnv enableConsoleEnvVar
|
|
|
|
maybe (return False) (parseAsBool . T.toLower) mVal
|
|
|
|
where
|
|
|
|
enableConsoleEnvVar = "HASURA_GRAPHQL_ENABLE_CONSOLE"
|
|
|
|
truthVals = ["true", "t", "yes", "y"]
|
|
|
|
falseVals = ["false", "f", "no", "n"]
|
|
|
|
|
|
|
|
parseAsBool t
|
|
|
|
| t `elem` truthVals = return True
|
|
|
|
| t `elem` falseVals = return False
|
|
|
|
| otherwise = putStrLn errMsg >> exitFailure
|
|
|
|
|
|
|
|
errMsg = "Fatal Error: " ++ enableConsoleEnvVar
|
|
|
|
++ " is not valid boolean text. " ++ "True values are "
|
|
|
|
++ show truthVals ++ " and False values are " ++ show falseVals
|
|
|
|
++ ". All values are case insensitive"
|
|
|
|
|
2018-06-27 16:11:32 +03:00
|
|
|
main :: IO ()
|
2018-07-20 10:22:46 +03:00
|
|
|
main = do
|
2018-06-27 16:11:32 +03:00
|
|
|
(RavenOptions rci ravenMode) <- parseArgs
|
2018-07-06 08:13:46 +03:00
|
|
|
mEnvDbUrl <- lookupEnv "HASURA_GRAPHQL_DATABASE_URL"
|
|
|
|
ci <- either ((>> exitFailure) . putStrLn . connInfoErrModifier)
|
|
|
|
return $ mkConnInfo mEnvDbUrl rci
|
2018-06-27 16:11:32 +03:00
|
|
|
printConnInfo ci
|
2018-09-27 14:22:49 +03:00
|
|
|
loggerCtx <- mkLoggerCtx $ defaultLoggerSettings True
|
|
|
|
hloggerCtx <- mkLoggerCtx $ defaultLoggerSettings False
|
2018-07-27 12:34:50 +03:00
|
|
|
httpManager <- HTTP.newManager HTTP.tlsManagerSettings
|
2018-06-27 16:11:32 +03:00
|
|
|
case ravenMode of
|
2018-09-27 14:22:49 +03:00
|
|
|
ROServe (ServeOptions port cp isoL mRootDir mAccessKey mWebHook mJwtSecret
|
2018-10-25 21:16:25 +03:00
|
|
|
mUnAuthRole corsCfg enableConsole) -> do
|
2018-09-27 14:22:49 +03:00
|
|
|
|
|
|
|
-- get all auth mode related config
|
add support for jwt authorization (close #186) (#255)
The API:
1. HGE has `--jwt-secret` flag or `HASURA_GRAPHQL_JWT_SECRET` env var. The value of which is a JSON.
2. The structure of this JSON is: `{"type": "<standard-JWT-algorithms>", "key": "<the-key>"}`
`type` : Standard JWT algos : `HS256`, `RS256`, `RS512` etc. (see jwt.io).
`key`:
i. Incase of symmetric key, the key as it is.
ii. Incase of asymmetric keys, only the public key, in a PEM encoded string or as a X509 certificate.
3. The claims in the JWT token must contain the following:
i. `x-hasura-default-role` field: default role of that user
ii. `x-hasura-allowed-roles` : A list of allowed roles for the user. The default role is overriden by `x-hasura-role` header.
4. The claims in the JWT token, can have other `x-hasura-*` fields where their values can only be strings.
5. The JWT tokens are sent as `Authorization: Bearer <token>` headers.
---
To test:
1. Generate a shared secret (for HMAC-SHA256) or RSA key pair.
2. Goto https://jwt.io/ , add the keys
3. Edit the claims to have `x-hasura-role` (mandatory) and other `x-hasura-*` fields. Add permissions related to the claims to test permissions.
4. Start HGE with `--jwt-secret` flag or `HASURA_GRAPHQL_JWT_SECRET` env var, which takes a JSON string: `{"type": "HS256", "key": "mylongsharedsecret"}` or `{"type":"RS256", "key": "<PEM-encoded-public-key>"}`
5. Copy the JWT token from jwt.io and use it in the `Authorization: Bearer <token>` header.
---
TODO: Support EC public keys. It is blocked on frasertweedale/hs-jose#61
2018-08-30 13:32:09 +03:00
|
|
|
mFinalAccessKey <- considerEnv "HASURA_GRAPHQL_ACCESS_KEY" $ getAccessKey <$> mAccessKey
|
|
|
|
mFinalWebHook <- considerEnv "HASURA_GRAPHQL_AUTH_HOOK" $ getWebhook <$> mWebHook
|
|
|
|
mFinalJwtSecret <- considerEnv "HASURA_GRAPHQL_JWT_SECRET" mJwtSecret
|
2018-10-25 21:16:25 +03:00
|
|
|
mFinalUnAuthRole <- considerEnv "HASURA_GRAPHQL_UNAUTHORIZED_ROLE" $ getRoleTxt <$> mUnAuthRole
|
2018-09-27 14:22:49 +03:00
|
|
|
-- prepare auth mode
|
|
|
|
authModeRes <- runExceptT $ mkAuthMode (AccessKey <$> mFinalAccessKey)
|
|
|
|
(Webhook <$> mFinalWebHook)
|
|
|
|
mFinalJwtSecret
|
2018-10-25 21:16:25 +03:00
|
|
|
(RoleName <$> mFinalUnAuthRole)
|
2018-09-27 14:22:49 +03:00
|
|
|
httpManager
|
|
|
|
loggerCtx
|
|
|
|
am <- either ((>> exitFailure) . putStrLn . T.unpack) return authModeRes
|
2018-07-06 08:13:46 +03:00
|
|
|
finalCorsDomain <- fromMaybe "*" <$> considerEnv "HASURA_GRAPHQL_CORS_DOMAIN" (ccDomain corsCfg)
|
2018-09-27 14:22:49 +03:00
|
|
|
let finalCorsCfg = CorsConfigG finalCorsDomain $ ccDisabled corsCfg
|
2018-10-10 09:46:38 +03:00
|
|
|
-- enable console config
|
|
|
|
finalEnableConsole <- bool getEnableConsoleEnv (return True) enableConsole
|
|
|
|
-- init catalog if necessary
|
2018-06-27 16:11:32 +03:00
|
|
|
initialise ci
|
2018-10-10 09:46:38 +03:00
|
|
|
-- migrate catalog if necessary
|
2018-06-27 16:11:32 +03:00
|
|
|
migrate ci
|
2018-09-05 14:26:46 +03:00
|
|
|
prepareEvents ci
|
2018-06-27 16:11:32 +03:00
|
|
|
pool <- Q.initPGPool ci cp
|
2018-07-20 10:22:46 +03:00
|
|
|
putStrLn $ "server: running on port " ++ show port
|
2018-10-10 09:46:38 +03:00
|
|
|
(app, cacheRef) <- mkWaiApp isoL mRootDir loggerCtx pool httpManager
|
|
|
|
am finalCorsCfg finalEnableConsole
|
2018-07-20 10:22:46 +03:00
|
|
|
let warpSettings = Warp.setPort port Warp.defaultSettings
|
|
|
|
-- Warp.setHost "*" Warp.defaultSettings
|
2018-07-27 12:34:50 +03:00
|
|
|
|
|
|
|
-- start a background thread to check for updates
|
|
|
|
void $ C.forkIO $ checkForUpdates loggerCtx httpManager
|
|
|
|
|
2018-09-05 14:26:46 +03:00
|
|
|
maxEvThrds <- getFromEnv defaultMaxEventThreads "HASURA_GRAPHQL_EVENTS_HTTP_POOL_SIZE"
|
2018-10-30 18:20:18 +03:00
|
|
|
evFetchMilliSec <- getFromEnv defaultFetchIntervalMilliSec "HASURA_GRAPHQL_EVENTS_FETCH_INTERVAL"
|
2018-10-26 19:28:03 +03:00
|
|
|
logEnvHeaders <- getFromEnv False "LOG_HEADERS_FROM_ENV"
|
2018-09-05 14:26:46 +03:00
|
|
|
|
2018-10-30 18:20:18 +03:00
|
|
|
eventEngineCtx <- atomically $ initEventEngineCtx maxEvThrds evFetchMilliSec
|
2018-09-05 14:26:46 +03:00
|
|
|
httpSession <- WrqS.newSessionControl Nothing TLS.tlsManagerSettings
|
|
|
|
|
2018-10-26 19:28:03 +03:00
|
|
|
void $ C.forkIO $ processEventQueue hloggerCtx logEnvHeaders httpSession pool cacheRef eventEngineCtx
|
2018-09-05 14:26:46 +03:00
|
|
|
|
2018-07-20 10:22:46 +03:00
|
|
|
Warp.runSettings warpSettings app
|
2018-07-27 12:34:50 +03:00
|
|
|
|
2018-06-27 16:11:32 +03:00
|
|
|
ROExport -> do
|
|
|
|
res <- runTx ci fetchMetadata
|
|
|
|
either ((>> exitFailure) . printJSON) printJSON res
|
|
|
|
ROClean -> do
|
|
|
|
res <- runTx ci cleanCatalog
|
|
|
|
either ((>> exitFailure) . printJSON) (const cleanSuccess) res
|
|
|
|
ROExecute -> do
|
|
|
|
queryBs <- BL.getContents
|
|
|
|
res <- runTx ci $ execQuery queryBs
|
|
|
|
either ((>> exitFailure) . printJSON) BLC.putStrLn res
|
|
|
|
where
|
|
|
|
runTx ci tx = do
|
|
|
|
pool <- getMinimalPool ci
|
|
|
|
runExceptT $ Q.runTx pool (Q.Serializable, Nothing) tx
|
|
|
|
getMinimalPool ci = do
|
|
|
|
let connParams = Q.defaultConnParams { Q.cpConns = 1 }
|
|
|
|
Q.initPGPool ci connParams
|
|
|
|
initialise ci = do
|
|
|
|
currentTime <- getCurrentTime
|
|
|
|
res <- runTx ci $ initCatalogSafe currentTime
|
|
|
|
either ((>> exitFailure) . printJSON) putStrLn res
|
|
|
|
migrate ci = do
|
|
|
|
currentTime <- getCurrentTime
|
|
|
|
res <- runTx ci $ migrateCatalog currentTime
|
|
|
|
either ((>> exitFailure) . printJSON) putStrLn res
|
2018-09-05 14:26:46 +03:00
|
|
|
prepareEvents ci = do
|
|
|
|
putStrLn "event_triggers: preparing data"
|
|
|
|
res <- runTx ci unlockAllEvents
|
|
|
|
either ((>> exitFailure) . printJSON) return res
|
|
|
|
getFromEnv :: (Read a) => a -> String -> IO a
|
|
|
|
getFromEnv defaults env = do
|
|
|
|
mEnv <- lookupEnv env
|
|
|
|
let mRes = case mEnv of
|
|
|
|
Nothing -> Just defaults
|
|
|
|
Just val -> readMaybe val
|
2018-10-26 19:28:03 +03:00
|
|
|
eRes = maybe (Left $ "Wrong expected type for environment variable: " <> env) Right mRes
|
2018-09-05 14:26:46 +03:00
|
|
|
either ((>> exitFailure) . putStrLn) return eRes
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2018-07-11 11:24:13 +03:00
|
|
|
cleanSuccess = putStrLn "successfully cleaned graphql-engine related data"
|
2018-06-27 16:11:32 +03:00
|
|
|
|
|
|
|
printConnInfo ci =
|
|
|
|
putStrLn $
|
|
|
|
"Postgres connection info:"
|
|
|
|
++ "\n Host: " ++ Q.connHost ci
|
|
|
|
++ "\n Port: " ++ show (Q.connPort ci)
|
|
|
|
++ "\n User: " ++ Q.connUser ci
|
|
|
|
++ "\n Database: " ++ Q.connDatabase ci
|
2018-07-06 08:13:46 +03:00
|
|
|
|
|
|
|
-- if flags given are Nothing consider it's value from Env
|
|
|
|
considerEnv _ (Just t) = return $ Just t
|
|
|
|
considerEnv e Nothing = fmap T.pack <$> lookupEnv e
|