这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ For ``serve`` subcommand these are the flags available
verifying. e.g: `{"type": "HS256", "key":
"<your-hmac-shared-secret>"}`,`{"type": "RS256",
"key": "<your-PEM-RSA-public-key>"}
--unauthorized-role Unauthorized role, used when access-key is not sent in access-key
only mode or "Authorization" header is absent in JWT mode
-s, --stripes Number of stripes
-c, --connections Number of connections that need to be opened to Postgres
--timeout Each connection's idle time before it is closed
Expand All @@ -60,11 +62,14 @@ Default environment variables

You can use environment variables to configure defaults instead of using flags:

.. note::
When the equivalent flags for environment variables are used, the flags will take precedence.

For example:

.. code-block:: bash

HASURA_GRAPHQL_DATABASE_URL=postgres://user:pass@host:5432/dbname graphql-engine serve
$ HASURA_GRAPHQL_DATABASE_URL=postgres://user:pass@host:5432/dbname graphql-engine serve


These are the environment variables which are available:
Expand All @@ -91,8 +96,6 @@ These are the environment variables which are available:
"key": "<your-PEM-RSA-public-key>"}
Enable JWT mode, the value of which is a JSON

HASURA_GRAPHQL_UNAUTHORIZED_ROLE Unauthorized role, used when access-key is not sent in access-key
only mode or "Authorization" header is absent in JWT mode
HASURA_GRAPHQL_ENABLE_CONSOLE Enable API console. It is served at '/' and '/console'


.. note::
When the equivalent flags for environment variables are used, the flags will take precedence.
67 changes: 7 additions & 60 deletions server/src-exec/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ module Main where
import Ops

import Control.Monad.STM (atomically)
import Data.IORef (newIORef)
import Data.Time.Clock (getCurrentTime)
import Options.Applicative
import System.Environment (lookupEnv)
Expand All @@ -18,18 +17,17 @@ 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.String.Conversions as CS
import qualified Data.Text as T
import qualified Data.Yaml as Y
import qualified Network.HTTP.Client as HTTP
import qualified Network.HTTP.Client.TLS as HTTP
import qualified Network.Wai.Handler.Warp as Warp

import Hasura.Events.Lib
import Hasura.Logging (LoggerCtx, defaultLoggerSettings,
mkLogger, mkLoggerCtx)
import Hasura.Logging (defaultLoggerSettings, mkLoggerCtx)
import Hasura.Prelude
import Hasura.RQL.DDL.Metadata (fetchMetadata)
import Hasura.RQL.Types (RoleName (..))
import Hasura.Server.App (mkWaiApp)
import Hasura.Server.Auth
import Hasura.Server.CheckUpdates (checkForUpdates)
Expand All @@ -54,6 +52,7 @@ data ServeOptions
, soAccessKey :: !(Maybe AccessKey)
, soWebHook :: !(Maybe Webhook)
, soJwtSecret :: !(Maybe Text)
, soUnAuthRole :: !(Maybe RoleName)
, soCorsConfig :: !CorsConfigFlags
, soEnableConsole :: !Bool
} deriving (Show, Eq)
Expand Down Expand Up @@ -86,6 +85,7 @@ parseRavenMode = subparser
<*> parseAccessKey
<*> parseWebHook
<*> parseJwtSecret
<*> parseUnAuthRole
<*> parseCorsConfig
<*> parseEnableConsole

Expand All @@ -103,61 +103,6 @@ printJSON = BLC.putStrLn . A.encode
printYaml :: (A.ToJSON a) => a -> IO ()
printYaml = BC.putStrLn . Y.encode

mkAuthMode
:: ( MonadIO m
, MonadError T.Text m
)
=> Maybe AccessKey
-> Maybe Webhook
-> Maybe T.Text
-> HTTP.Manager
-> LoggerCtx
-> m AuthMode
mkAuthMode mAccessKey mWebHook mJwtSecret httpManager lCtx =
case (mAccessKey, mWebHook, mJwtSecret) of
(Nothing, Nothing, Nothing) -> return AMNoAuth
(Just key, Nothing, Nothing) -> return $ AMAccessKey key
(Just key, Just hook, Nothing) -> return $ AMAccessKeyAndHook key hook
(Just key, Nothing, Just jwtConf) ->
AMAccessKeyAndJWT key <$> mkJwtCtx jwtConf httpManager lCtx

(Nothing, Just _, Nothing) -> throwError $
"Fatal Error : --auth-hook (HASURA_GRAPHQL_AUTH_HOOK)"
<> " requires --access-key (HASURA_GRAPHQL_ACCESS_KEY) to be set"
(Nothing, Nothing, Just _) -> throwError $
"Fatal Error : --jwt-secret (HASURA_GRAPHQL_JWT_SECRET)"
<> " requires --access-key (HASURA_GRAPHQL_ACCESS_KEY) to be set"
(Nothing, Just _, Just _) -> throwError
"Fatal Error: Both webhook and JWT mode cannot be enabled at the same time"
(Just _, Just _, Just _) -> throwError
"Fatal Error: Both webhook and JWT mode cannot be enabled at the same time"

mkJwtCtx
:: ( MonadIO m
, MonadError T.Text m
)
=> T.Text
-> HTTP.Manager
-> LoggerCtx
-> m JWTCtx
mkJwtCtx jwtConf httpManager loggerCtx = do
-- the JWT Conf as JSON string; try to parse it
conf <- either decodeErr return $ A.eitherDecodeStrict $ CS.cs jwtConf
jwkRef <- case jcKeyOrUrl conf of
Left jwk -> liftIO $ newIORef (JWKSet [jwk])
Right url -> do
ref <- liftIO $ newIORef $ JWKSet []
let logger = mkLogger loggerCtx
mTime <- updateJwkRef logger httpManager url ref
case mTime of
Nothing -> return ref
Just t -> do
jwkRefreshCtrl logger httpManager url ref t
return ref
return $ JWTCtx jwkRef (jcClaimNs conf) (jcAudience conf)
where
decodeErr e = throwError . T.pack $ "Fatal Error: JWT conf: " <> e

getEnableConsoleEnv :: IO Bool
getEnableConsoleEnv = do
mVal <- fmap T.pack <$> lookupEnv enableConsoleEnvVar
Expand Down Expand Up @@ -189,16 +134,18 @@ main = do
httpManager <- HTTP.newManager HTTP.tlsManagerSettings
case ravenMode of
ROServe (ServeOptions port cp isoL mRootDir mAccessKey mWebHook mJwtSecret
corsCfg enableConsole) -> do
mUnAuthRole corsCfg enableConsole) -> do

-- get all auth mode related config
mFinalAccessKey <- considerEnv "HASURA_GRAPHQL_ACCESS_KEY" $ getAccessKey <$> mAccessKey
mFinalWebHook <- considerEnv "HASURA_GRAPHQL_AUTH_HOOK" $ getWebhook <$> mWebHook
mFinalJwtSecret <- considerEnv "HASURA_GRAPHQL_JWT_SECRET" mJwtSecret
mFinalUnAuthRole <- considerEnv "HASURA_GRAPHQL_UNAUTHORIZED_ROLE" $ getRoleTxt <$> mUnAuthRole
-- prepare auth mode
authModeRes <- runExceptT $ mkAuthMode (AccessKey <$> mFinalAccessKey)
(Webhook <$> mFinalWebHook)
mFinalJwtSecret
(RoleName <$> mFinalUnAuthRole)
httpManager
loggerCtx
am <- either ((>> exitFailure) . putStrLn . T.unpack) return authModeRes
Expand Down
105 changes: 88 additions & 17 deletions server/src-lib/Hasura/Server/Auth.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
module Hasura.Server.Auth
( getUserInfo
, AuthMode(..)
, mkAuthMode
, AccessKey (..)
, Webhook (..)
-- JWT related
Expand All @@ -21,25 +22,28 @@ module Hasura.Server.Auth
, jwkRefreshCtrl
) where

import Control.Exception (try)
import Control.Exception (try)
import Control.Lens
import Data.Aeson
import Data.CaseInsensitive (CI (..), original)

import qualified Data.ByteString.Lazy as BL
import qualified Data.HashMap.Strict as M
import qualified Data.Text as T
import qualified Network.HTTP.Client as H
import qualified Network.HTTP.Types as N
import qualified Network.Wreq as Wreq

import Data.CaseInsensitive (CI (..), original)
import Data.IORef (newIORef)

import qualified Data.ByteString.Lazy as BL
import qualified Data.HashMap.Strict as M
import qualified Data.String.Conversions as CS
import qualified Data.Text as T
import qualified Network.HTTP.Client as H
import qualified Network.HTTP.Types as N
import qualified Network.Wreq as Wreq

import Hasura.Logging
import Hasura.Prelude
import Hasura.RQL.Types
import Hasura.Server.Auth.JWT
import Hasura.Server.Logging
import Hasura.Server.Utils

import qualified Hasura.Logging as L
import qualified Hasura.Logging as L


newtype AccessKey
Expand All @@ -52,11 +56,73 @@ newtype Webhook

data AuthMode
= AMNoAuth
| AMAccessKey !AccessKey
| AMAccessKey !AccessKey !(Maybe RoleName)
| AMAccessKeyAndHook !AccessKey !Webhook
| AMAccessKeyAndJWT !AccessKey !JWTCtx
| AMAccessKeyAndJWT !AccessKey !JWTCtx !(Maybe RoleName)
deriving (Show, Eq)

mkAuthMode
:: ( MonadIO m
, MonadError T.Text m
)
=> Maybe AccessKey
-> Maybe Webhook
-> Maybe T.Text
-> Maybe RoleName
-> H.Manager
-> LoggerCtx
-> m AuthMode
mkAuthMode mAccessKey mWebHook mJwtSecret mUnAuthRole httpManager lCtx =
case (mAccessKey, mWebHook, mJwtSecret) of
(Nothing, Nothing, Nothing) -> return AMNoAuth
(Just key, Nothing, Nothing) -> return $ AMAccessKey key mUnAuthRole
(Just key, Just hook, Nothing) -> unAuthRoleNotReqForWebHook >>
return (AMAccessKeyAndHook key hook)
(Just key, Nothing, Just jwtConf) -> do
jwtCtx <- mkJwtCtx jwtConf httpManager lCtx
return $ AMAccessKeyAndJWT key jwtCtx mUnAuthRole

(Nothing, Just _, Nothing) -> throwError $
"Fatal Error : --auth-hook (HASURA_GRAPHQL_AUTH_HOOK)"
<> " requires --access-key (HASURA_GRAPHQL_ACCESS_KEY) to be set"
(Nothing, Nothing, Just _) -> throwError $
"Fatal Error : --jwt-secret (HASURA_GRAPHQL_JWT_SECRET)"
<> " requires --access-key (HASURA_GRAPHQL_ACCESS_KEY) to be set"
(Nothing, Just _, Just _) -> throwError
"Fatal Error: Both webhook and JWT mode cannot be enabled at the same time"
(Just _, Just _, Just _) -> throwError
"Fatal Error: Both webhook and JWT mode cannot be enabled at the same time"
where
unAuthRoleNotReqForWebHook =
when (isJust mUnAuthRole) $
throwError $ "Fatal Error: --unauthorized-role (HASURA_GRAPHQL_UNAUTHORIZED_ROLE) is not allowed"
<> " when --auth-hook (HASURA_GRAPHQL_AUTH_HOOK) is set"

mkJwtCtx
:: ( MonadIO m
, MonadError T.Text m
)
=> T.Text
-> H.Manager
-> LoggerCtx
-> m JWTCtx
mkJwtCtx jwtConf httpManager loggerCtx = do
-- the JWT Conf as JSON string; try to parse it
conf <- either decodeErr return $ eitherDecodeStrict $ CS.cs jwtConf
jwkRef <- case jcKeyOrUrl conf of
Left jwk -> liftIO $ newIORef (JWKSet [jwk])
Right url -> do
ref <- liftIO $ newIORef $ JWKSet []
let logger = mkLogger loggerCtx
mTime <- updateJwkRef logger httpManager url ref
case mTime of
Nothing -> return ref
Just t -> do
jwkRefreshCtrl logger httpManager url ref t
return ref
return $ JWTCtx jwkRef (jcClaimNs conf) (jcAudience conf)
where
decodeErr e = throwError . T.pack $ "Fatal Error: JWT conf: " <> e

mkUserInfoFromResp
:: (MonadIO m, MonadError QErr m)
Expand Down Expand Up @@ -145,16 +211,16 @@ getUserInfo logger manager rawHeaders = \case

AMNoAuth -> return userInfoFromHeaders

AMAccessKey accKey ->
AMAccessKey accKey unAuthRole ->
case getHeader accessKeyHeader of
Just givenAccKey -> userInfoWhenAccessKey accKey givenAccKey
Nothing -> throw401 $ accessKeyHeader <> " required, but not found"
Nothing -> userInfoWhenNoAccessKey unAuthRole

AMAccessKeyAndHook accKey hook ->
whenAccessKeyAbsent accKey (userInfoFromWebhook logger manager hook rawHeaders)

AMAccessKeyAndJWT accKey jwtSecret ->
whenAccessKeyAbsent accKey (processJwt jwtSecret rawHeaders)
AMAccessKeyAndJWT accKey jwtSecret unAuthRole ->
whenAccessKeyAbsent accKey (processJwt jwtSecret rawHeaders unAuthRole)

where
-- when access key is absent, run the action to retrieve UserInfo, otherwise
Expand All @@ -178,3 +244,8 @@ getUserInfo logger manager rawHeaders = \case
userInfoWhenAccessKey key reqKey = do
when (reqKey /= getAccessKey key) $ throw401 $ "invalid " <> accessKeyHeader
return userInfoFromHeaders

userInfoWhenNoAccessKey = \case
Nothing -> throw401 $ accessKeyHeader <> " required, but not found"
Just role -> return $ UserInfo role $
M.insertWith const userRoleHeader (getRoleTxt role) headers
34 changes: 27 additions & 7 deletions server/src-lib/Hasura/Server/Auth/JWT.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module Hasura.Server.Auth.JWT
, jwkRefreshCtrl
) where

import Control.Arrow (first)
import Control.Exception (try)
import Control.Lens
import Control.Monad (when)
Expand Down Expand Up @@ -164,8 +165,31 @@ processJwt
, MonadError QErr m)
=> JWTCtx
-> HTTP.RequestHeaders
-> Maybe RoleName
-> m UserInfo
processJwt jwtCtx headers = do
processJwt jwtCtx headers mUnAuthRole =
maybe withoutAuthZHeader withAuthZHeader mAuthZHeader
where
mAuthZHeader = find (\h -> fst h == CI.mk "Authorization") headers

withAuthZHeader (_, authzHeader) =
processAuthZHeader jwtCtx headers $ BL.fromStrict authzHeader

withoutAuthZHeader = do
unAuthRole <- maybe missingAuthzHeader return mUnAuthRole
return $ UserInfo unAuthRole
$ Map.singleton userRoleHeader $ getRoleTxt unAuthRole
missingAuthzHeader =
throw400 InvalidHeaders "Missing Authorization header in JWT authentication mode"

processAuthZHeader
:: ( MonadIO m
, MonadError QErr m)
=> JWTCtx
-> HTTP.RequestHeaders
-> BLC.ByteString
-> m UserInfo
processAuthZHeader jwtCtx headers authzHeader = do
-- try to parse JWT token from Authorization header
jwt <- parseAuthzHeader

Expand All @@ -182,7 +206,7 @@ processJwt jwtCtx headers = do

-- filter only x-hasura claims and convert to lower-case
let claimsMap = Map.filterWithKey (\k _ -> T.isPrefixOf "x-hasura-" k)
$ Map.fromList $ map (\(k, v) -> (T.toLower k, v))
$ Map.fromList $ map (first T.toLower)
$ Map.toList hasuraClaims

HasuraClaims allowedRoles defaultRole <- parseHasuraClaims claimsMap
Expand All @@ -203,9 +227,7 @@ processJwt jwtCtx headers = do

where
parseAuthzHeader = do
let mAuthzHeader = find (\h -> fst h == CI.mk "Authorization") headers
(_, authzHeader) <- maybe missingAuthzHeader return mAuthzHeader
let tokenParts = BLC.words $ BL.fromStrict authzHeader
let tokenParts = BLC.words authzHeader
case tokenParts of
["Bearer", jwt] -> return jwt
_ -> malformedAuthzHeader
Expand Down Expand Up @@ -235,8 +257,6 @@ processJwt jwtCtx headers = do

malformedAuthzHeader =
throw400 InvalidHeaders "Malformed Authorization header"
missingAuthzHeader =
throw400 InvalidHeaders "Missing Authorization header in JWT authentication mode"
currRoleNotAllowed =
throw400 AccessDenied "Your current role is not in allowed roles"
claimsNotFound = do
Expand Down
Loading