graphql-engine/server/src-lib/Hasura/Server/Auth.hs
2020-05-22 14:40:57 +05:30

200 lines
7.0 KiB
Haskell

{-# LANGUAGE RecordWildCards #-}
module Hasura.Server.Auth
( getUserInfo
, getUserInfoWithExpTime
, AuthMode (..)
, mkAuthMode
, AdminSecret (..)
-- WebHook related
, AuthHookType (..)
, AuthHookG (..)
, AuthHook
-- JWT related
, RawJWT
, JWTConfig (..)
, JWTCtx (..)
, JWKSet (..)
, processJwt
, updateJwkRef
, UserAuthentication (..)
) where
import Control.Concurrent.Extended (forkImmortal)
import Data.IORef (newIORef)
import Data.Time.Clock (UTCTime)
import Hasura.Server.Version (HasVersion)
import qualified Data.Text as T
import qualified Network.HTTP.Client as H
import qualified Network.HTTP.Types as N
import Hasura.Logging
import Hasura.Prelude
import Hasura.RQL.Types
import Hasura.Server.Auth.JWT
import Hasura.Server.Auth.WebHook
import Hasura.Server.Utils
import Hasura.Session
-- | Typeclass representing the @UserInfo@ authorization and resolving effect
class (Monad m) => UserAuthentication m where
resolveUserInfo
:: HasVersion
=> Logger Hasura
-> H.Manager
-> [N.Header]
-- ^ request headers
-> AuthMode
-> m (Either QErr (UserInfo, Maybe UTCTime))
newtype AdminSecret
= AdminSecret { getAdminSecret :: T.Text }
deriving (Show, Eq)
data AuthMode
= AMNoAuth
| AMAdminSecret !AdminSecret !(Maybe RoleName)
| AMAdminSecretAndHook !AdminSecret !AuthHook
| AMAdminSecretAndJWT !AdminSecret !JWTCtx !(Maybe RoleName)
deriving (Show, Eq)
mkAuthMode
:: ( HasVersion
, MonadIO m
, MonadError T.Text m
)
=> Maybe AdminSecret
-> Maybe AuthHook
-> Maybe JWTConfig
-> Maybe RoleName
-> H.Manager
-> Logger Hasura
-> m AuthMode
mkAuthMode mAdminSecret mWebHook mJwtSecret mUnAuthRole httpManager logger =
case (mAdminSecret, mWebHook, mJwtSecret) of
(Nothing, Nothing, Nothing) -> return AMNoAuth
(Just key, Nothing, Nothing) -> return $ AMAdminSecret key mUnAuthRole
(Just key, Just hook, Nothing) -> unAuthRoleNotReqForWebHook >>
return (AMAdminSecretAndHook key hook)
(Just key, Nothing, Just jwtConf) -> do
jwtCtx <- mkJwtCtx jwtConf httpManager logger
return $ AMAdminSecretAndJWT key jwtCtx mUnAuthRole
(Nothing, Just _, Nothing) -> throwError $
"Fatal Error : --auth-hook (HASURA_GRAPHQL_AUTH_HOOK)" <> requiresAdminScrtMsg
(Nothing, Nothing, Just _) -> throwError $
"Fatal Error : --jwt-secret (HASURA_GRAPHQL_JWT_SECRET)" <> requiresAdminScrtMsg
(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
requiresAdminScrtMsg =
" requires --admin-secret (HASURA_GRAPHQL_ADMIN_SECRET) or "
<> " --access-key (HASURA_GRAPHQL_ACCESS_KEY) to be set"
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"
-- | Given the 'JWTConfig' (the user input of JWT configuration), create the 'JWTCtx' (the runtime JWT config used)
mkJwtCtx
:: ( HasVersion
, MonadIO m
, MonadError T.Text m
)
=> JWTConfig
-> H.Manager
-> Logger Hasura
-> m JWTCtx
mkJwtCtx JWTConfig{..} httpManager logger = do
jwkRef <- case jcKeyOrUrl of
Left jwk -> liftIO $ newIORef (JWKSet [jwk])
Right url -> getJwkFromUrl url
let claimsFmt = fromMaybe JCFJson jcClaimsFormat
return $ JWTCtx jwkRef jcClaimNs jcAudience claimsFmt jcIssuer
where
-- if we can't find any expiry time for the JWK (either in @Expires@ header or @Cache-Control@
-- header), do not start a background thread for refreshing the JWK
getJwkFromUrl url = do
ref <- liftIO $ newIORef $ JWKSet []
maybeExpiry <- withJwkError $ updateJwkRef logger httpManager url ref
case maybeExpiry of
Nothing -> return ref
Just time -> do
void $ liftIO $ forkImmortal "jwkRefreshCtrl" logger $
jwkRefreshCtrl logger httpManager url ref (convertDuration time)
return ref
withJwkError act = do
res <- runExceptT act
case res of
Right r -> return r
Left err -> case err of
-- when fetching JWK initially, except expiry parsing error, all errors are critical
JFEHttpException _ msg -> throwError msg
JFEHttpError _ _ _ e -> throwError e
JFEJwkParseError _ e -> throwError e
JFEExpiryParseError _ _ -> return Nothing
getUserInfo
:: (HasVersion, MonadIO m, MonadError QErr m)
=> Logger Hasura
-> H.Manager
-> [N.Header]
-> AuthMode
-> m UserInfo
getUserInfo l m r a = fst <$> getUserInfoWithExpTime l m r a
getUserInfoWithExpTime
:: forall m. (HasVersion, MonadIO m, MonadError QErr m)
=> Logger Hasura
-> H.Manager
-> [N.Header]
-> AuthMode
-> m (UserInfo, Maybe UTCTime)
getUserInfoWithExpTime logger manager rawHeaders = \case
AMNoAuth -> withNoExpTime $ mkUserInfoFallbackAdminRole UAuthNotSet
AMAdminSecret adminSecretSet maybeUnauthRole ->
withAuthorization adminSecretSet $ withNoExpTime $
-- Consider unauthorized role, if not found raise admin secret header required exception
case maybeUnauthRole of
Nothing -> throw401 $ adminSecretHeader <> "/"
<> deprecatedAccessKeyHeader <> " required, but not found"
Just unAuthRole ->
mkUserInfo (URBPreDetermined unAuthRole) UAdminSecretNotSent sessionVariables
AMAdminSecretAndHook adminSecretSet hook ->
withAuthorization adminSecretSet $ userInfoFromAuthHook logger manager hook rawHeaders
AMAdminSecretAndJWT adminSecretSet jwtSecret unAuthRole ->
withAuthorization adminSecretSet $ processJwt jwtSecret rawHeaders unAuthRole
where
mkUserInfoFallbackAdminRole adminSecretState =
mkUserInfo (URBFromSessionVariablesFallback adminRoleName)
adminSecretState sessionVariables
sessionVariables = mkSessionVariables rawHeaders
withAuthorization
:: AdminSecret -> m (UserInfo, Maybe UTCTime) -> m (UserInfo, Maybe UTCTime)
withAuthorization adminSecretSet actionIfNoAdminSecret = do
let maybeRequestAdminSecret =
foldl1 (<|>) $ map (`getSessionVariableValue` sessionVariables)
[adminSecretHeader, deprecatedAccessKeyHeader]
-- when admin secret is absent, run the action to retrieve UserInfo
case maybeRequestAdminSecret of
Nothing -> actionIfNoAdminSecret
Just requestAdminSecret -> do
when (requestAdminSecret /= getAdminSecret adminSecretSet) $ throw401 $
"invalid " <> adminSecretHeader <> "/" <> deprecatedAccessKeyHeader
withNoExpTime $ mkUserInfoFallbackAdminRole UAdminSecretSent
withNoExpTime a = (, Nothing) <$> a