2018-06-27 16:11:32 +03:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
|
|
|
|
module Hasura.Server.Utils where
|
|
|
|
|
2018-06-28 13:49:40 +03:00
|
|
|
import qualified Database.PG.Query.Connection as Q
|
|
|
|
|
|
|
|
import Data.List.Split
|
|
|
|
import Network.URI
|
2018-07-03 18:34:25 +03:00
|
|
|
import System.Exit
|
|
|
|
import System.Process
|
2018-06-28 13:49:40 +03:00
|
|
|
|
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 Data.ByteString as B
|
2018-06-28 13:49:40 +03:00
|
|
|
import qualified Data.Text as T
|
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 Data.Text.Encoding as TE
|
|
|
|
import qualified Data.Text.Encoding.Error as TE
|
2018-07-03 18:34:25 +03:00
|
|
|
import qualified Data.Text.IO as TI
|
|
|
|
import qualified Language.Haskell.TH.Syntax as TH
|
|
|
|
|
2018-06-27 16:11:32 +03:00
|
|
|
import Hasura.Prelude
|
|
|
|
|
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
|
|
|
|
2018-06-27 16:11:32 +03:00
|
|
|
dropAndSnakeCase :: T.Text -> T.Text
|
|
|
|
dropAndSnakeCase = T.drop 9 . toSnakeCase . T.toLower
|
|
|
|
|
|
|
|
toSnakeCase :: T.Text -> T.Text
|
|
|
|
toSnakeCase = T.pack . map change . T.unpack
|
|
|
|
where
|
|
|
|
change '-' = '_'
|
|
|
|
change c = c
|
|
|
|
|
|
|
|
isXHasuraTxt :: T.Text -> Bool
|
|
|
|
isXHasuraTxt = T.isInfixOf "x-hasura-" . T.toLower
|
|
|
|
|
2018-07-03 18:34:25 +03:00
|
|
|
jsonHeader :: (T.Text, T.Text)
|
|
|
|
jsonHeader = ("Content-Type", "application/json; charset=utf-8")
|
|
|
|
|
2018-06-27 16:11:32 +03:00
|
|
|
userRoleHeader :: T.Text
|
|
|
|
userRoleHeader = "x-hasura-role"
|
|
|
|
|
|
|
|
accessKeyHeader :: T.Text
|
|
|
|
accessKeyHeader = "x-hasura-access-key"
|
2018-06-28 13:49:40 +03:00
|
|
|
|
2018-08-29 08:47:13 +03:00
|
|
|
userIdHeader :: T.Text
|
|
|
|
userIdHeader = "x-hasura-user-id"
|
|
|
|
|
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
|
|
|
bsToTxt :: B.ByteString -> T.Text
|
|
|
|
bsToTxt = TE.decodeUtf8With TE.lenientDecode
|
|
|
|
|
2018-06-28 13:49:40 +03:00
|
|
|
-- Parsing postgres database url
|
|
|
|
-- from: https://github.com/futurice/postgresql-simple-url/
|
|
|
|
parseDatabaseUrl :: String -> Maybe String -> Maybe Q.ConnInfo
|
|
|
|
parseDatabaseUrl databaseUrl opts = parseURI databaseUrl >>= uriToConnectInfo opts
|
|
|
|
|
|
|
|
uriToConnectInfo :: Maybe String -> URI -> Maybe Q.ConnInfo
|
|
|
|
uriToConnectInfo opts uri
|
|
|
|
| uriScheme uri /= "postgres:" && uriScheme uri /= "postgresql:" = Nothing
|
|
|
|
| otherwise = ($ Q.defaultConnInfo {Q.connOptions = opts}) <$> mkConnectInfo uri
|
|
|
|
|
|
|
|
type ConnectInfoChange = Q.ConnInfo -> Q.ConnInfo
|
|
|
|
|
|
|
|
mkConnectInfo :: URI -> Maybe ConnectInfoChange
|
|
|
|
mkConnectInfo uri = case uriPath uri of
|
|
|
|
('/' : rest) | not (null rest) -> Just $ uriParameters uri
|
|
|
|
_ -> Nothing
|
|
|
|
|
|
|
|
uriParameters :: URI -> ConnectInfoChange
|
|
|
|
uriParameters uri = (\info -> info { Q.connDatabase = tail $ uriPath uri }) . maybe id uriAuthParameters (uriAuthority uri)
|
|
|
|
|
|
|
|
dropLast :: [a] -> [a]
|
|
|
|
dropLast [] = []
|
|
|
|
dropLast [_] = []
|
|
|
|
dropLast (x:xs) = x : dropLast xs
|
|
|
|
|
|
|
|
uriAuthParameters :: URIAuth -> ConnectInfoChange
|
|
|
|
uriAuthParameters uriAuth = port . host . auth
|
|
|
|
where port = case uriPort uriAuth of
|
|
|
|
(':' : p) -> \info -> info { Q.connPort = read p }
|
|
|
|
_ -> id
|
|
|
|
host = case uriRegName uriAuth of
|
2018-09-12 09:19:08 +03:00
|
|
|
h -> \info -> info { Q.connHost = unEscapeString h }
|
2018-06-28 13:49:40 +03:00
|
|
|
auth = case splitOn ":" (uriUserInfo uriAuth) of
|
|
|
|
[""] -> id
|
2018-09-12 09:19:08 +03:00
|
|
|
[u] -> \info -> info { Q.connUser = unEscapeString $ dropLast u }
|
|
|
|
[u, p] -> \info -> info { Q.connUser = unEscapeString u, Q.connPassword = unEscapeString $ dropLast p }
|
2018-06-28 13:49:40 +03:00
|
|
|
_ -> id
|
2018-07-03 18:34:25 +03:00
|
|
|
|
|
|
|
-- Running shell script during compile time
|
|
|
|
runScript :: FilePath -> TH.Q TH.Exp
|
|
|
|
runScript fp = do
|
|
|
|
TH.addDependentFile fp
|
|
|
|
fileContent <- TH.runIO $ TI.readFile fp
|
|
|
|
(exitCode, stdOut, stdErr) <- TH.runIO $
|
|
|
|
readProcessWithExitCode "/bin/sh" [] $ T.unpack fileContent
|
|
|
|
when (exitCode /= ExitSuccess) $ fail $
|
|
|
|
"Running shell script " ++ fp ++ " failed with exit code : "
|
|
|
|
++ show exitCode ++ " and with error : " ++ stdErr
|
|
|
|
TH.lift stdOut
|