2018-12-21 10:51:02 +03:00
|
|
|
{-# LANGUAGE CPP #-}
|
2018-12-13 10:26:15 +03:00
|
|
|
{-# LANGUAGE DataKinds #-}
|
|
|
|
{-# LANGUAGE RankNTypes #-}
|
2018-06-27 16:11:32 +03:00
|
|
|
|
|
|
|
module Hasura.Server.App where
|
|
|
|
|
2018-09-14 16:27:46 +03:00
|
|
|
import Control.Arrow ((***))
|
2018-06-27 16:11:32 +03:00
|
|
|
import Control.Concurrent.MVar
|
2018-07-20 10:22:46 +03:00
|
|
|
import Data.Aeson hiding (json)
|
2018-11-23 16:02:46 +03:00
|
|
|
import Data.IORef
|
2018-07-20 10:22:46 +03:00
|
|
|
import Data.Time.Clock (UTCTime,
|
|
|
|
getCurrentTime)
|
|
|
|
import Network.Wai (requestHeaders,
|
|
|
|
strictRequestBody)
|
2019-03-25 11:56:29 +03:00
|
|
|
import System.Exit (exitFailure)
|
2018-06-27 16:11:32 +03:00
|
|
|
import Web.Spock.Core
|
|
|
|
|
2018-11-23 16:02:46 +03:00
|
|
|
import qualified Data.ByteString.Lazy as BL
|
2018-12-21 10:51:02 +03:00
|
|
|
#ifdef LocalConsole
|
|
|
|
import qualified Data.FileEmbed as FE
|
|
|
|
#endif
|
2018-11-23 16:02:46 +03:00
|
|
|
import qualified Data.HashMap.Strict as M
|
2019-02-28 16:53:03 +03:00
|
|
|
import qualified Data.HashSet as S
|
2018-11-23 16:02:46 +03:00
|
|
|
import qualified Data.Text as T
|
2018-07-20 10:22:46 +03:00
|
|
|
import qualified Network.HTTP.Client as HTTP
|
2018-11-23 16:02:46 +03:00
|
|
|
import qualified Network.HTTP.Types as N
|
|
|
|
import qualified Network.Wai as Wai
|
|
|
|
import qualified Network.Wai.Handler.WebSockets as WS
|
|
|
|
import qualified Network.WebSockets as WS
|
|
|
|
import qualified Text.Mustache as M
|
|
|
|
import qualified Text.Mustache.Compile as M
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2018-07-20 10:22:46 +03:00
|
|
|
import qualified Database.PG.Query as Q
|
2019-04-17 12:48:41 +03:00
|
|
|
import qualified Hasura.GraphQL.Execute as E
|
|
|
|
import qualified Hasura.GraphQL.Execute.LiveQuery as EL
|
2018-10-19 05:15:28 +03:00
|
|
|
import qualified Hasura.GraphQL.Explain as GE
|
2018-07-20 10:22:46 +03:00
|
|
|
import qualified Hasura.GraphQL.Schema as GS
|
|
|
|
import qualified Hasura.GraphQL.Transport.HTTP as GH
|
|
|
|
import qualified Hasura.GraphQL.Transport.HTTP.Protocol as GH
|
|
|
|
import qualified Hasura.GraphQL.Transport.WebSocket as WS
|
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
|
|
|
import qualified Hasura.Logging as L
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2019-03-18 19:22:21 +03:00
|
|
|
import Hasura.EncJSON
|
2018-07-20 10:22:46 +03:00
|
|
|
import Hasura.Prelude hiding (get, put)
|
2019-04-17 19:29:39 +03:00
|
|
|
import Hasura.RQL.DDL.RemoteSchema
|
2018-06-27 16:11:32 +03:00
|
|
|
import Hasura.RQL.DDL.Schema.Table
|
|
|
|
import Hasura.RQL.DML.QueryTemplate
|
|
|
|
import Hasura.RQL.Types
|
2018-09-14 16:27:46 +03:00
|
|
|
import Hasura.Server.Auth (AuthMode (..),
|
|
|
|
getUserInfo)
|
2019-02-14 08:58:38 +03:00
|
|
|
import Hasura.Server.Cors
|
2018-06-27 16:11:32 +03:00
|
|
|
import Hasura.Server.Init
|
|
|
|
import Hasura.Server.Logging
|
2019-02-14 08:58:38 +03:00
|
|
|
import Hasura.Server.Middleware (corsMiddleware)
|
2019-04-30 11:34:08 +03:00
|
|
|
import qualified Hasura.Server.PGDump as PGD
|
2018-06-27 16:11:32 +03:00
|
|
|
import Hasura.Server.Query
|
|
|
|
import Hasura.Server.Utils
|
2018-07-03 18:34:25 +03:00
|
|
|
import Hasura.Server.Version
|
2018-06-27 16:11:32 +03:00
|
|
|
import Hasura.SQL.Types
|
|
|
|
|
2018-07-03 18:34:25 +03:00
|
|
|
consoleTmplt :: M.Template
|
|
|
|
consoleTmplt = $(M.embedSingleTemplate "src-rsr/console.html")
|
|
|
|
|
2019-01-28 16:55:28 +03:00
|
|
|
boolToText :: Bool -> T.Text
|
|
|
|
boolToText = bool "false" "true"
|
|
|
|
|
2019-02-14 12:37:47 +03:00
|
|
|
isAdminSecretSet :: AuthMode -> T.Text
|
|
|
|
isAdminSecretSet AMNoAuth = boolToText False
|
|
|
|
isAdminSecretSet _ = boolToText True
|
2018-09-14 16:27:46 +03:00
|
|
|
|
2018-12-21 10:51:02 +03:00
|
|
|
#ifdef LocalConsole
|
|
|
|
consoleAssetsLoc :: Text
|
|
|
|
consoleAssetsLoc = "/static"
|
|
|
|
#else
|
|
|
|
consoleAssetsLoc :: Text
|
|
|
|
consoleAssetsLoc =
|
|
|
|
"https://storage.googleapis.com/hasura-graphql-engine/console/" <> consoleVersion
|
|
|
|
#endif
|
|
|
|
|
2019-01-28 16:55:28 +03:00
|
|
|
mkConsoleHTML :: T.Text -> AuthMode -> Bool -> Either String T.Text
|
|
|
|
mkConsoleHTML path authMode enableTelemetry =
|
2018-12-18 12:39:01 +03:00
|
|
|
bool (Left errMsg) (Right res) $ null errs
|
2018-07-03 18:34:25 +03:00
|
|
|
where
|
|
|
|
(errs, res) = M.checkedSubstitute consoleTmplt $
|
2018-12-21 10:51:02 +03:00
|
|
|
object [ "consoleAssetsLoc" .= consoleAssetsLoc
|
2019-02-14 12:37:47 +03:00
|
|
|
, "isAdminSecretSet" .= isAdminSecretSet authMode
|
2018-12-21 10:51:02 +03:00
|
|
|
, "consolePath" .= consolePath
|
2019-01-28 16:55:28 +03:00
|
|
|
, "enableTelemetry" .= boolToText enableTelemetry
|
2018-12-21 10:51:02 +03:00
|
|
|
]
|
2018-12-18 12:39:01 +03:00
|
|
|
consolePath = case path of
|
|
|
|
"" -> "/console"
|
|
|
|
r -> "/console/" <> r
|
|
|
|
|
|
|
|
errMsg = "console template rendering failed: " ++ show errs
|
2018-06-29 14:05:09 +03:00
|
|
|
|
2019-03-12 08:46:27 +03:00
|
|
|
data SchemaCacheRef
|
|
|
|
= SchemaCacheRef
|
2019-04-17 12:48:41 +03:00
|
|
|
{ _scrLock :: MVar ()
|
|
|
|
, _scrCache :: IORef (SchemaCache, SchemaCacheVer)
|
|
|
|
-- an action to run when schemacache changes
|
|
|
|
, _scrOnChange :: IO ()
|
2019-03-12 08:46:27 +03:00
|
|
|
}
|
|
|
|
|
2019-04-17 19:29:39 +03:00
|
|
|
getSCFromRef :: SchemaCacheRef -> IO SchemaCache
|
|
|
|
getSCFromRef scRef = fst <$> readIORef (_scrCache scRef)
|
|
|
|
|
2019-04-29 09:22:48 +03:00
|
|
|
logInconsObjs :: L.Logger -> [InconsistentMetadataObj] -> IO ()
|
|
|
|
logInconsObjs logger objs =
|
|
|
|
unless (null objs) $ L.unLogger logger $ mkInconsMetadataLog objs
|
|
|
|
|
2019-03-12 08:46:27 +03:00
|
|
|
withSCUpdate
|
|
|
|
:: (MonadIO m, MonadError e m)
|
2019-04-29 09:22:48 +03:00
|
|
|
=> SchemaCacheRef -> L.Logger -> m (a, SchemaCache) -> m a
|
|
|
|
withSCUpdate scr logger action = do
|
2019-03-12 08:46:27 +03:00
|
|
|
acquireLock
|
|
|
|
(res, newSC) <- action `catchError` onError
|
2019-04-29 09:22:48 +03:00
|
|
|
liftIO $ do
|
|
|
|
-- update schemacache in IO reference
|
|
|
|
modifyIORef' cacheRef $
|
|
|
|
\(_, prevVer) -> (newSC, incSchemaCacheVer prevVer)
|
|
|
|
-- log any inconsistent objects
|
|
|
|
logInconsObjs logger $ scInconsistentObjs newSC
|
|
|
|
onChange
|
2019-03-12 08:46:27 +03:00
|
|
|
releaseLock
|
|
|
|
return res
|
|
|
|
where
|
2019-04-17 12:48:41 +03:00
|
|
|
SchemaCacheRef lk cacheRef onChange = scr
|
2019-03-12 08:46:27 +03:00
|
|
|
onError e = releaseLock >> throwError e
|
|
|
|
acquireLock = liftIO $ takeMVar lk
|
|
|
|
releaseLock = liftIO $ putMVar lk ()
|
|
|
|
|
2018-06-27 16:11:32 +03:00
|
|
|
data ServerCtx
|
|
|
|
= ServerCtx
|
2019-05-14 14:20:55 +03:00
|
|
|
{ scPGExecCtx :: PGExecCtx
|
|
|
|
, scConnInfo :: Q.ConnInfo
|
|
|
|
, scLogger :: L.Logger
|
|
|
|
, scCacheRef :: SchemaCacheRef
|
|
|
|
, scAuthMode :: AuthMode
|
|
|
|
, scManager :: HTTP.Manager
|
|
|
|
, scSQLGenCtx :: SQLGenCtx
|
|
|
|
, scEnabledAPIs :: S.HashSet API
|
|
|
|
, scInstanceId :: InstanceId
|
|
|
|
, scPlanCache :: E.PlanCache
|
|
|
|
, scLQState :: EL.LiveQueriesState
|
2018-06-27 16:11:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
data HandlerCtx
|
|
|
|
= HandlerCtx
|
2018-11-23 16:02:46 +03:00
|
|
|
{ hcServerCtx :: ServerCtx
|
|
|
|
, hcReqBody :: BL.ByteString
|
|
|
|
, hcUser :: UserInfo
|
|
|
|
, hcReqHeaders :: [N.Header]
|
2018-06-27 16:11:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
type Handler = ExceptT QErr (ReaderT HandlerCtx IO)
|
|
|
|
|
2019-04-30 11:34:08 +03:00
|
|
|
data APIResp
|
|
|
|
= JSONResp !EncJSON
|
|
|
|
| RawResp !T.Text !BL.ByteString -- content-type, body
|
|
|
|
|
|
|
|
apiRespToLBS :: APIResp -> BL.ByteString
|
|
|
|
apiRespToLBS = \case
|
|
|
|
JSONResp j -> encJToLBS j
|
|
|
|
RawResp _ b -> b
|
|
|
|
|
|
|
|
mkAPIRespHandler :: Handler EncJSON -> Handler APIResp
|
|
|
|
mkAPIRespHandler = fmap JSONResp
|
|
|
|
|
2019-02-28 16:53:03 +03:00
|
|
|
isMetadataEnabled :: ServerCtx -> Bool
|
|
|
|
isMetadataEnabled sc = S.member METADATA $ scEnabledAPIs sc
|
|
|
|
|
|
|
|
isGraphQLEnabled :: ServerCtx -> Bool
|
|
|
|
isGraphQLEnabled sc = S.member GRAPHQL $ scEnabledAPIs sc
|
|
|
|
|
2019-04-30 11:34:08 +03:00
|
|
|
isPGDumpEnabled :: ServerCtx -> Bool
|
|
|
|
isPGDumpEnabled sc = S.member PGDUMP $ scEnabledAPIs sc
|
|
|
|
|
2019-04-30 08:15:23 +03:00
|
|
|
isDeveloperAPIEnabled :: ServerCtx -> Bool
|
|
|
|
isDeveloperAPIEnabled sc = S.member DEVELOPER $ scEnabledAPIs sc
|
|
|
|
|
2018-07-20 10:22:46 +03:00
|
|
|
-- {-# SCC parseBody #-}
|
2018-06-27 16:11:32 +03:00
|
|
|
parseBody :: (FromJSON a) => Handler a
|
|
|
|
parseBody = do
|
|
|
|
reqBody <- hcReqBody <$> ask
|
|
|
|
case decode' reqBody of
|
|
|
|
Just jVal -> decodeValue jVal
|
|
|
|
Nothing -> throw400 InvalidJSON "invalid json"
|
|
|
|
|
|
|
|
onlyAdmin :: Handler ()
|
|
|
|
onlyAdmin = do
|
2018-07-20 10:22:46 +03:00
|
|
|
uRole <- asks (userRole . hcUser)
|
2018-06-27 16:11:32 +03:00
|
|
|
when (uRole /= adminRole) $
|
|
|
|
throw400 AccessDenied "You have to be an admin to access this endpoint"
|
|
|
|
|
|
|
|
buildQCtx :: Handler QCtx
|
|
|
|
buildQCtx = do
|
|
|
|
scRef <- scCacheRef . hcServerCtx <$> ask
|
2018-07-20 10:22:46 +03:00
|
|
|
userInfo <- asks hcUser
|
2019-04-17 12:48:41 +03:00
|
|
|
cache <- fmap fst $ liftIO $ readIORef $ _scrCache scRef
|
2019-04-17 19:29:39 +03:00
|
|
|
sqlGenCtx <- scSQLGenCtx . hcServerCtx <$> ask
|
|
|
|
return $ QCtx userInfo cache sqlGenCtx
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2018-07-06 08:16:42 +03:00
|
|
|
logResult
|
|
|
|
:: (MonadIO m)
|
2018-10-25 12:37:57 +03:00
|
|
|
=> Maybe UserInfo -> Wai.Request -> BL.ByteString -> ServerCtx
|
2018-08-03 11:43:35 +03:00
|
|
|
-> Either QErr BL.ByteString -> Maybe (UTCTime, UTCTime)
|
|
|
|
-> m ()
|
2018-10-25 12:37:57 +03:00
|
|
|
logResult userInfoM req reqBody sc res qTime =
|
|
|
|
liftIO $ logger $ mkAccessLog userInfoM req (reqBody, res) qTime
|
2018-07-06 08:16:42 +03:00
|
|
|
where
|
2018-09-27 14:22:49 +03:00
|
|
|
logger = L.unLogger $ scLogger sc
|
2018-07-06 08:16:42 +03:00
|
|
|
|
2018-08-03 11:43:35 +03:00
|
|
|
logError
|
|
|
|
:: MonadIO m
|
2018-10-25 12:37:57 +03:00
|
|
|
=> Maybe UserInfo -> Wai.Request
|
|
|
|
-> BL.ByteString -> ServerCtx -> QErr -> m ()
|
|
|
|
logError userInfoM req reqBody sc qErr =
|
|
|
|
logResult userInfoM req reqBody sc (Left qErr) Nothing
|
2018-07-09 09:04:41 +03:00
|
|
|
|
2018-06-27 16:11:32 +03:00
|
|
|
mkSpockAction
|
|
|
|
:: (MonadIO m)
|
2018-07-20 10:22:46 +03:00
|
|
|
=> (Bool -> QErr -> Value)
|
2019-05-10 09:05:11 +03:00
|
|
|
-> (QErr -> QErr)
|
2018-06-27 16:11:32 +03:00
|
|
|
-> ServerCtx
|
2019-04-30 11:34:08 +03:00
|
|
|
-> Handler APIResp
|
2018-06-27 16:11:32 +03:00
|
|
|
-> ActionT m ()
|
2019-05-10 09:05:11 +03:00
|
|
|
mkSpockAction qErrEncoder qErrModifier serverCtx handler = do
|
2018-06-27 16:11:32 +03:00
|
|
|
req <- request
|
|
|
|
reqBody <- liftIO $ strictRequestBody req
|
2018-07-20 10:22:46 +03:00
|
|
|
let headers = requestHeaders req
|
|
|
|
authMode = scAuthMode serverCtx
|
|
|
|
manager = scManager serverCtx
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2018-08-03 11:43:35 +03:00
|
|
|
userInfoE <- liftIO $ runExceptT $ getUserInfo logger manager headers authMode
|
2019-05-14 14:20:55 +03:00
|
|
|
userInfo <- either (logAndThrow req reqBody False . qErrModifier) return userInfoE
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2018-11-23 16:02:46 +03:00
|
|
|
let handlerState = HandlerCtx serverCtx reqBody userInfo headers
|
2018-06-27 16:11:32 +03:00
|
|
|
|
|
|
|
t1 <- liftIO getCurrentTime -- for measuring response time purposes
|
2019-05-10 09:05:11 +03:00
|
|
|
result <- liftIO $ runReaderT (runExceptT handler) handlerState
|
2018-06-27 16:11:32 +03:00
|
|
|
t2 <- liftIO getCurrentTime -- for measuring response time purposes
|
|
|
|
|
2019-05-10 09:05:11 +03:00
|
|
|
-- apply the error modifier
|
|
|
|
let modResult = fmapL qErrModifier result
|
2019-03-18 19:22:21 +03:00
|
|
|
|
2018-07-06 08:16:42 +03:00
|
|
|
-- log result
|
2019-05-10 09:05:11 +03:00
|
|
|
logResult (Just userInfo) req reqBody serverCtx (apiRespToLBS <$> modResult) $ Just (t1, t2)
|
|
|
|
either (qErrToResp $ userRole userInfo == adminRole) resToResp modResult
|
2018-07-20 10:22:46 +03:00
|
|
|
|
2018-06-27 16:11:32 +03:00
|
|
|
where
|
2018-10-25 12:37:57 +03:00
|
|
|
logger = scLogger serverCtx
|
2018-06-27 16:11:32 +03:00
|
|
|
-- encode error response
|
2018-12-13 10:26:15 +03:00
|
|
|
qErrToResp :: (MonadIO m) => Bool -> QErr -> ActionCtxT ctx m b
|
2018-07-20 10:22:46 +03:00
|
|
|
qErrToResp includeInternal qErr = do
|
2018-06-27 16:11:32 +03:00
|
|
|
setStatus $ qeStatus qErr
|
2018-07-20 10:22:46 +03:00
|
|
|
json $ qErrEncoder includeInternal qErr
|
2018-07-09 09:04:41 +03:00
|
|
|
|
2018-08-03 11:43:35 +03:00
|
|
|
logAndThrow req reqBody includeInternal qErr = do
|
2018-10-25 12:37:57 +03:00
|
|
|
logError Nothing req reqBody serverCtx qErr
|
2018-07-20 10:22:46 +03:00
|
|
|
qErrToResp includeInternal qErr
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2019-05-10 09:05:11 +03:00
|
|
|
resToResp = \case
|
|
|
|
JSONResp j -> do
|
|
|
|
uncurry setHeader jsonHeader
|
|
|
|
lazyBytes $ encJToLBS j
|
|
|
|
RawResp ct b -> do
|
|
|
|
setHeader "content-type" ct
|
|
|
|
lazyBytes b
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2019-03-18 19:22:21 +03:00
|
|
|
v1QueryHandler :: RQLQuery -> Handler EncJSON
|
2018-06-27 16:11:32 +03:00
|
|
|
v1QueryHandler query = do
|
2019-03-12 08:46:27 +03:00
|
|
|
scRef <- scCacheRef . hcServerCtx <$> ask
|
2019-04-29 09:22:48 +03:00
|
|
|
logger <- scLogger . hcServerCtx <$> ask
|
|
|
|
bool (fst <$> dbAction) (withSCUpdate scRef logger dbActionReload) $
|
2018-06-27 16:11:32 +03:00
|
|
|
queryNeedsReload query
|
|
|
|
where
|
|
|
|
-- Hit postgres
|
|
|
|
dbAction = do
|
2018-07-20 10:22:46 +03:00
|
|
|
userInfo <- asks hcUser
|
2018-06-27 16:11:32 +03:00
|
|
|
scRef <- scCacheRef . hcServerCtx <$> ask
|
2019-04-17 12:48:41 +03:00
|
|
|
schemaCache <- fmap fst $ liftIO $ readIORef $ _scrCache scRef
|
2018-11-23 16:02:46 +03:00
|
|
|
httpMgr <- scManager . hcServerCtx <$> ask
|
2019-04-17 19:29:39 +03:00
|
|
|
sqlGenCtx <- scSQLGenCtx . hcServerCtx <$> ask
|
2019-04-17 12:48:41 +03:00
|
|
|
pgExecCtx <- scPGExecCtx . hcServerCtx <$> ask
|
2019-03-12 08:46:27 +03:00
|
|
|
instanceId <- scInstanceId . hcServerCtx <$> ask
|
2019-04-17 19:29:39 +03:00
|
|
|
runQuery pgExecCtx instanceId userInfo schemaCache httpMgr sqlGenCtx query
|
2018-06-27 16:11:32 +03:00
|
|
|
|
|
|
|
-- Also update the schema cache
|
|
|
|
dbActionReload = do
|
|
|
|
(resp, newSc) <- dbAction
|
2018-11-23 16:02:46 +03:00
|
|
|
httpMgr <- scManager . hcServerCtx <$> ask
|
|
|
|
--FIXME: should we be fetching the remote schema again? if not how do we get the remote schema?
|
2019-04-17 19:29:39 +03:00
|
|
|
newSc' <- GS.updateSCWithGCtx newSc >>= flip resolveRemoteSchemas httpMgr
|
2019-03-12 08:46:27 +03:00
|
|
|
return (resp, newSc')
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2019-04-17 12:48:41 +03:00
|
|
|
v1Alpha1GQHandler :: GH.GQLReqUnparsed -> Handler EncJSON
|
2018-06-27 16:11:32 +03:00
|
|
|
v1Alpha1GQHandler query = do
|
2018-07-20 10:22:46 +03:00
|
|
|
userInfo <- asks hcUser
|
2018-11-23 16:02:46 +03:00
|
|
|
reqBody <- asks hcReqBody
|
|
|
|
reqHeaders <- asks hcReqHeaders
|
|
|
|
manager <- scManager . hcServerCtx <$> ask
|
2018-06-27 16:11:32 +03:00
|
|
|
scRef <- scCacheRef . hcServerCtx <$> ask
|
2019-04-17 12:48:41 +03:00
|
|
|
(sc, scVer) <- liftIO $ readIORef $ _scrCache scRef
|
|
|
|
pgExecCtx <- scPGExecCtx . hcServerCtx <$> ask
|
2019-04-17 19:29:39 +03:00
|
|
|
sqlGenCtx <- scSQLGenCtx . hcServerCtx <$> ask
|
2019-04-17 12:48:41 +03:00
|
|
|
planCache <- scPlanCache . hcServerCtx <$> ask
|
2019-04-17 19:29:39 +03:00
|
|
|
GH.runGQ pgExecCtx userInfo sqlGenCtx planCache
|
2019-04-17 12:48:41 +03:00
|
|
|
sc scVer manager reqHeaders query reqBody
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2019-05-10 09:05:11 +03:00
|
|
|
v1GQHandler :: GH.GQLReqUnparsed -> Handler EncJSON
|
|
|
|
v1GQHandler = v1Alpha1GQHandler
|
|
|
|
|
2019-03-18 19:22:21 +03:00
|
|
|
gqlExplainHandler :: GE.GQLExplain -> Handler EncJSON
|
2018-10-19 05:15:28 +03:00
|
|
|
gqlExplainHandler query = do
|
|
|
|
onlyAdmin
|
|
|
|
scRef <- scCacheRef . hcServerCtx <$> ask
|
2019-04-17 12:48:41 +03:00
|
|
|
sc <- fmap fst $ liftIO $ readIORef $ _scrCache scRef
|
|
|
|
pgExecCtx <- scPGExecCtx . hcServerCtx <$> ask
|
2019-04-17 19:29:39 +03:00
|
|
|
sqlGenCtx <- scSQLGenCtx . hcServerCtx <$> ask
|
|
|
|
GE.explainGQLQuery pgExecCtx sc sqlGenCtx query
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2019-04-30 11:34:08 +03:00
|
|
|
v1Alpha1PGDumpHandler :: PGD.PGDumpReqBody -> Handler APIResp
|
|
|
|
v1Alpha1PGDumpHandler b = do
|
|
|
|
onlyAdmin
|
|
|
|
ci <- scConnInfo . hcServerCtx <$> ask
|
|
|
|
output <- PGD.execPGDump b ci
|
|
|
|
return $ RawResp "application/sql" output
|
|
|
|
|
2018-06-27 16:11:32 +03:00
|
|
|
newtype QueryParser
|
|
|
|
= QueryParser { getQueryParser :: QualifiedTable -> Handler RQLQuery }
|
|
|
|
|
|
|
|
queryParsers :: M.HashMap T.Text QueryParser
|
|
|
|
queryParsers =
|
|
|
|
M.fromList
|
|
|
|
[ ("select", mkQueryParser RQSelect)
|
|
|
|
, ("insert", mkQueryParser RQInsert)
|
|
|
|
, ("update", mkQueryParser RQUpdate)
|
|
|
|
, ("delete", mkQueryParser RQDelete)
|
|
|
|
, ("count", mkQueryParser RQCount)
|
|
|
|
]
|
|
|
|
where
|
|
|
|
mkQueryParser f =
|
|
|
|
QueryParser $ \qt -> do
|
|
|
|
obj <- parseBody
|
|
|
|
let val = Object $ M.insert "table" (toJSON qt) obj
|
|
|
|
q <- decodeValue val
|
|
|
|
return $ f q
|
|
|
|
|
2019-03-18 19:22:21 +03:00
|
|
|
legacyQueryHandler :: TableName -> T.Text -> Handler EncJSON
|
2018-06-27 16:11:32 +03:00
|
|
|
legacyQueryHandler tn queryType =
|
|
|
|
case M.lookup queryType queryParsers of
|
|
|
|
Just queryParser -> getQueryParser queryParser qt >>= v1QueryHandler
|
2018-07-20 10:22:46 +03:00
|
|
|
Nothing -> throw404 "No such resource exists"
|
2018-06-27 16:11:32 +03:00
|
|
|
where
|
2019-01-25 06:31:54 +03:00
|
|
|
qt = QualifiedObject publicSchema tn
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2019-03-25 11:56:29 +03:00
|
|
|
initErrExit :: QErr -> IO a
|
|
|
|
initErrExit e = do
|
|
|
|
putStrLn $
|
|
|
|
"failed to build schema-cache because of inconsistent metadata: "
|
|
|
|
<> T.unpack (qeError e)
|
|
|
|
exitFailure
|
2018-11-23 16:02:46 +03:00
|
|
|
|
2018-07-20 10:22:46 +03:00
|
|
|
mkWaiApp
|
2019-04-17 19:29:39 +03:00
|
|
|
:: Q.TxIsolation -> L.LoggerCtx -> SQLGenCtx
|
2019-04-30 11:34:08 +03:00
|
|
|
-> Q.PGPool -> Q.ConnInfo -> HTTP.Manager -> AuthMode
|
2019-03-12 08:46:27 +03:00
|
|
|
-> CorsConfig -> Bool -> Bool
|
|
|
|
-> InstanceId -> S.HashSet API
|
2019-04-17 12:48:41 +03:00
|
|
|
-> EL.LQOpts
|
2019-03-12 08:46:27 +03:00
|
|
|
-> IO (Wai.Application, SchemaCacheRef, Maybe UTCTime)
|
2019-04-30 11:34:08 +03:00
|
|
|
mkWaiApp isoLevel loggerCtx sqlGenCtx pool ci httpManager mode corsCfg
|
2019-04-17 12:48:41 +03:00
|
|
|
enableConsole enableTelemetry instanceId apis
|
|
|
|
lqOpts = do
|
|
|
|
let pgExecCtx = PGExecCtx pool isoLevel
|
|
|
|
pgExecCtxSer = PGExecCtx pool Q.Serializable
|
2019-03-12 08:46:27 +03:00
|
|
|
(cacheRef, cacheBuiltTime) <- do
|
2019-01-29 13:09:58 +03:00
|
|
|
pgResp <- runExceptT $ peelRun emptySchemaCache adminUserInfo
|
2019-04-17 19:29:39 +03:00
|
|
|
httpManager sqlGenCtx pgExecCtxSer $ do
|
2019-03-12 08:46:27 +03:00
|
|
|
buildSchemaCache
|
|
|
|
liftTx fetchLastUpdate
|
|
|
|
(time, sc) <- either initErrExit return pgResp
|
2019-04-17 12:48:41 +03:00
|
|
|
scRef <- newIORef (sc, initSchemaCacheVer)
|
2019-03-12 08:46:27 +03:00
|
|
|
return (scRef, snd <$> time)
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2018-07-20 10:22:46 +03:00
|
|
|
cacheLock <- newMVar ()
|
2019-04-17 12:48:41 +03:00
|
|
|
planCache <- E.initPlanCache
|
2018-07-20 10:22:46 +03:00
|
|
|
|
2019-04-17 12:48:41 +03:00
|
|
|
let corsPolicy = mkDefaultCorsPolicy corsCfg
|
|
|
|
logger = L.mkLogger loggerCtx
|
2018-07-20 10:22:46 +03:00
|
|
|
|
2019-04-17 12:48:41 +03:00
|
|
|
lqState <- EL.initLiveQueriesState lqOpts pgExecCtx
|
|
|
|
wsServerEnv <- WS.createWSServerEnv logger pgExecCtx lqState
|
|
|
|
cacheRef httpManager corsPolicy sqlGenCtx planCache
|
2018-07-20 10:22:46 +03:00
|
|
|
|
2019-04-17 12:48:41 +03:00
|
|
|
let schemaCacheRef =
|
|
|
|
SchemaCacheRef cacheLock cacheRef (E.clearPlanCache planCache)
|
2019-04-30 11:34:08 +03:00
|
|
|
serverCtx = ServerCtx pgExecCtx ci logger
|
2019-04-17 12:48:41 +03:00
|
|
|
schemaCacheRef mode httpManager
|
2019-04-17 19:29:39 +03:00
|
|
|
sqlGenCtx apis instanceId planCache lqState
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2019-04-17 12:48:41 +03:00
|
|
|
spockApp <- spockAsApp $ spockT id $
|
|
|
|
httpApp corsCfg serverCtx enableConsole enableTelemetry
|
2019-03-04 10:46:53 +03:00
|
|
|
|
2018-07-20 10:22:46 +03:00
|
|
|
let wsServerApp = WS.createWSServerApp mode wsServerEnv
|
2019-03-12 08:46:27 +03:00
|
|
|
return ( WS.websocketsOr WS.defaultConnectionOptions wsServerApp spockApp
|
|
|
|
, schemaCacheRef
|
|
|
|
, cacheBuiltTime
|
|
|
|
)
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2019-01-28 16:55:28 +03:00
|
|
|
httpApp :: CorsConfig -> ServerCtx -> Bool -> Bool -> SpockT IO ()
|
|
|
|
httpApp corsCfg serverCtx enableConsole enableTelemetry = do
|
2018-06-27 16:11:32 +03:00
|
|
|
-- cors middleware
|
2019-02-14 08:58:38 +03:00
|
|
|
unless (isCorsDisabled corsCfg) $
|
|
|
|
middleware $ corsMiddleware (mkDefaultCorsPolicy corsCfg)
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2018-06-29 14:05:09 +03:00
|
|
|
-- API Console and Root Dir
|
2019-02-28 16:53:03 +03:00
|
|
|
when (enableConsole && enableMetadata) serveApiConsole
|
2018-06-29 14:05:09 +03:00
|
|
|
|
2019-04-17 19:29:39 +03:00
|
|
|
-- Health check endpoint
|
|
|
|
get "healthz" $ do
|
|
|
|
sc <- liftIO $ getSCFromRef $ scCacheRef serverCtx
|
2019-05-10 09:05:11 +03:00
|
|
|
if null $ scInconsistentObjs sc
|
|
|
|
then setStatus N.status200 >> lazyBytes "OK"
|
|
|
|
else setStatus N.status500 >> lazyBytes "ERROR"
|
2019-04-17 19:29:39 +03:00
|
|
|
|
2018-07-27 12:34:50 +03:00
|
|
|
get "v1/version" $ do
|
|
|
|
uncurry setHeader jsonHeader
|
|
|
|
lazyBytes $ encode $ object [ "version" .= currentVersion ]
|
2018-07-03 18:34:25 +03:00
|
|
|
|
2019-02-28 16:53:03 +03:00
|
|
|
when enableMetadata $ do
|
|
|
|
get ("v1/template" <//> var) tmpltGetOrDeleteH
|
|
|
|
post ("v1/template" <//> var) tmpltPutOrPostH
|
|
|
|
put ("v1/template" <//> var) tmpltPutOrPostH
|
|
|
|
delete ("v1/template" <//> var) tmpltGetOrDeleteH
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2019-05-10 09:05:11 +03:00
|
|
|
post "v1/query" $ mkSpockAction encodeQErr id serverCtx $ mkAPIRespHandler $ do
|
2019-02-28 16:53:03 +03:00
|
|
|
query <- parseBody
|
|
|
|
v1QueryHandler query
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2019-02-28 16:53:03 +03:00
|
|
|
post ("api/1/table" <//> var <//> var) $ \tableName queryType ->
|
2019-05-10 09:05:11 +03:00
|
|
|
mkSpockAction encodeQErr id serverCtx $ mkAPIRespHandler $
|
2019-02-28 16:53:03 +03:00
|
|
|
legacyQueryHandler (TableName tableName) queryType
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2019-04-30 11:34:08 +03:00
|
|
|
when enablePGDump $
|
2019-05-10 09:05:11 +03:00
|
|
|
post "v1alpha1/pg_dump" $ mkSpockAction encodeQErr id serverCtx $ do
|
2019-04-30 11:34:08 +03:00
|
|
|
query <- parseBody
|
|
|
|
v1Alpha1PGDumpHandler query
|
|
|
|
|
2019-02-28 16:53:03 +03:00
|
|
|
when enableGraphQL $ do
|
2019-05-10 09:05:11 +03:00
|
|
|
post "v1alpha1/graphql/explain" gqlExplainAction
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2019-05-10 09:05:11 +03:00
|
|
|
post "v1alpha1/graphql" $ mkSpockAction GH.encodeGQErr id serverCtx $
|
2019-04-30 11:34:08 +03:00
|
|
|
mkAPIRespHandler $ do
|
|
|
|
query <- parseBody
|
|
|
|
v1Alpha1GQHandler query
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2019-05-10 09:05:11 +03:00
|
|
|
post "v1/graphql/explain" gqlExplainAction
|
|
|
|
|
|
|
|
post "v1/graphql" $ mkSpockAction GH.encodeGQErr allMod200 serverCtx $
|
|
|
|
mkAPIRespHandler $ do
|
|
|
|
query <- parseBody
|
|
|
|
v1GQHandler query
|
|
|
|
|
2019-04-30 08:15:23 +03:00
|
|
|
when (isDeveloperAPIEnabled serverCtx) $ do
|
2019-05-10 09:05:11 +03:00
|
|
|
get "dev/plan_cache" $ mkSpockAction encodeQErr id serverCtx $
|
2019-04-30 11:34:08 +03:00
|
|
|
mkAPIRespHandler $ do
|
|
|
|
onlyAdmin
|
|
|
|
respJ <- liftIO $ E.dumpPlanCache $ scPlanCache serverCtx
|
|
|
|
return $ encJFromJValue respJ
|
2019-05-10 09:05:11 +03:00
|
|
|
get "dev/subscriptions" $ mkSpockAction encodeQErr id serverCtx $
|
2019-04-30 11:34:08 +03:00
|
|
|
mkAPIRespHandler $ do
|
|
|
|
onlyAdmin
|
|
|
|
respJ <- liftIO $ EL.dumpLiveQueriesState False $ scLQState serverCtx
|
|
|
|
return $ encJFromJValue respJ
|
2019-05-10 09:05:11 +03:00
|
|
|
get "dev/subscriptions/extended" $ mkSpockAction encodeQErr id serverCtx $
|
2019-04-30 11:34:08 +03:00
|
|
|
mkAPIRespHandler $ do
|
|
|
|
onlyAdmin
|
|
|
|
respJ <- liftIO $ EL.dumpLiveQueriesState True $ scLQState serverCtx
|
|
|
|
return $ encJFromJValue respJ
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2019-02-28 16:53:03 +03:00
|
|
|
forM_ [GET,POST] $ \m -> hookAny m $ \_ -> do
|
2018-07-06 08:16:42 +03:00
|
|
|
let qErr = err404 NotFound "resource does not exist"
|
2018-12-18 12:39:01 +03:00
|
|
|
raiseGenericApiError qErr
|
2018-06-27 16:11:32 +03:00
|
|
|
|
|
|
|
where
|
2019-05-10 09:05:11 +03:00
|
|
|
-- all graphql errors should be of type 200
|
|
|
|
allMod200 qe = qe { qeStatus = N.status200 }
|
|
|
|
|
|
|
|
gqlExplainAction =
|
|
|
|
mkSpockAction encodeQErr id serverCtx $ mkAPIRespHandler $ do
|
|
|
|
expQuery <- parseBody
|
|
|
|
gqlExplainHandler expQuery
|
|
|
|
|
2019-02-28 16:53:03 +03:00
|
|
|
enableGraphQL = isGraphQLEnabled serverCtx
|
|
|
|
enableMetadata = isMetadataEnabled serverCtx
|
2019-04-30 11:34:08 +03:00
|
|
|
enablePGDump = isPGDumpEnabled serverCtx
|
2018-07-20 10:22:46 +03:00
|
|
|
tmpltGetOrDeleteH tmpltName = do
|
2018-06-27 16:11:32 +03:00
|
|
|
tmpltArgs <- tmpltArgsFromQueryParams
|
2019-05-10 09:05:11 +03:00
|
|
|
mkSpockAction encodeQErr id serverCtx $ mkAPIRespHandler $
|
2019-04-30 11:34:08 +03:00
|
|
|
mkQTemplateAction tmpltName tmpltArgs
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2018-07-20 10:22:46 +03:00
|
|
|
tmpltPutOrPostH tmpltName = do
|
2018-06-27 16:11:32 +03:00
|
|
|
tmpltArgs <- tmpltArgsFromQueryParams
|
2019-05-10 09:05:11 +03:00
|
|
|
mkSpockAction encodeQErr id serverCtx $ mkAPIRespHandler $ do
|
2018-06-27 16:11:32 +03:00
|
|
|
bodyTmpltArgs <- parseBody
|
|
|
|
mkQTemplateAction tmpltName $ M.union bodyTmpltArgs tmpltArgs
|
|
|
|
|
|
|
|
tmpltArgsFromQueryParams = do
|
|
|
|
qparams <- params
|
|
|
|
return $ M.fromList $ flip map qparams $
|
2018-09-14 16:27:46 +03:00
|
|
|
TemplateParam *** String
|
2018-06-27 16:11:32 +03:00
|
|
|
|
|
|
|
mkQTemplateAction tmpltName tmpltArgs =
|
|
|
|
v1QueryHandler $ RQExecuteQueryTemplate $
|
|
|
|
ExecQueryTemplate (TQueryName tmpltName) tmpltArgs
|
2018-06-29 14:05:09 +03:00
|
|
|
|
2018-12-18 12:39:01 +03:00
|
|
|
raiseGenericApiError qErr = do
|
|
|
|
req <- request
|
|
|
|
reqBody <- liftIO $ strictRequestBody req
|
|
|
|
logError Nothing req reqBody serverCtx qErr
|
|
|
|
uncurry setHeader jsonHeader
|
|
|
|
setStatus $ qeStatus qErr
|
|
|
|
lazyBytes $ encode qErr
|
|
|
|
|
|
|
|
serveApiConsole = do
|
|
|
|
get root $ redirect "console"
|
|
|
|
get ("console" <//> wildcard) $ \path ->
|
|
|
|
either (raiseGenericApiError . err500 Unexpected . T.pack) html $
|
2019-01-28 16:55:28 +03:00
|
|
|
mkConsoleHTML path (scAuthMode serverCtx) enableTelemetry
|
2018-12-21 10:51:02 +03:00
|
|
|
|
|
|
|
#ifdef LocalConsole
|
|
|
|
get "static/main.js" $ do
|
|
|
|
setHeader "Content-Type" "text/javascript;charset=UTF-8"
|
|
|
|
bytes $(FE.embedFile "../console/static/dist/main.js")
|
|
|
|
get "static/main.css" $ do
|
|
|
|
setHeader "Content-Type" "text/css;charset=UTF-8"
|
|
|
|
bytes $(FE.embedFile "../console/static/dist/main.css")
|
|
|
|
get "static/vendor.js" $ do
|
|
|
|
setHeader "Content-Type" "text/javascript;charset=UTF-8"
|
|
|
|
bytes $(FE.embedFile "../console/static/dist/vendor.js")
|
|
|
|
#endif
|