clean up user variables parsing logic and fix explain api (#869)

This commit is contained in:
Vamshi Surabhi 2018-10-26 21:27:22 +05:30 committed by GitHub
parent fb842fde6f
commit 8b0082eac1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 209 additions and 86 deletions

View File

@ -6448,9 +6448,9 @@
}
},
"hasura-console-graphiql": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/hasura-console-graphiql/-/hasura-console-graphiql-0.0.6.tgz",
"integrity": "sha512-9gdwhtrsE3tkwfoWEy/3qOhvB3k16G8w7fF/ReLXNl+f2b3HhBZM5tXYYI9vN/v85vufx611E6uZO3AeU0IaOQ==",
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/hasura-console-graphiql/-/hasura-console-graphiql-0.0.8.tgz",
"integrity": "sha512-0V5xqb/8BBAufLaQr517pcROee0RnhW1yRWVhogKCts8oKDTyZknsbiC95PC3e+kvALVfwZ3HyMuR18eW/Dnyg==",
"requires": {
"codemirror": "^5.26.0",
"codemirror-graphql": "^0.7.1",

View File

@ -59,7 +59,7 @@
"deep-equal": "^1.0.1",
"graphiql": "^0.11.11",
"graphql": "^0.13.2",
"hasura-console-graphiql": "0.0.6",
"hasura-console-graphiql": "0.0.8",
"history": "^3.0.0",
"hoist-non-react-statics": "^1.0.3",
"invariant": "^2.2.0",

View File

@ -227,15 +227,21 @@ const graphQLFetcherFinal = (graphQLParams, url, headers) => {
};
/* Analyse Fetcher */
const analyzeFetcher = (url, headers) => {
const analyzeFetcher = (url, headers, analyzeApiChange) => {
return query => {
const editedQuery = {
query,
};
const user = {};
let user = {};
const reqHeaders = getHeadersAsJSON(headers);
user.role = 'admin';
user.headers = reqHeaders;
if (!analyzeApiChange) {
user.role = 'admin';
user.headers = reqHeaders;
} else {
user = {
'x-hasura-role': 'admin',
};
}
editedQuery.user = user;
return fetch(`${url}/explain`, {
method: 'post',

View File

@ -758,7 +758,6 @@ div.CodeMirror-lint-tooltip > * + * {
background: rgb(255, 255, 255);
border-radius: 4px;
outline: none;
overflow: -webkit-paged-y;
}
.myOverlayClass {
position: fixed;
@ -808,11 +807,11 @@ div.CodeMirror-lint-tooltip > * + * {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
height: 100%;
height: calc(100% - 46px);
}
.topLevelNodesWrapper {
border-right: 1px solid #ccc;
height: calc(100% - 42px);
height: 100%;
}
.textCenter {
@ -827,7 +826,9 @@ div.CodeMirror-lint-tooltip > * + * {
font-size: 16px;
}
.analysisWrapper {
height: calc(100% - 30px);
height: 100%;
position: relative;
width: 100%;
}
.topLevelNodesWrapper ul {
-webkit-padding-start: 0px;
@ -861,30 +862,53 @@ div.CodeMirror-lint-tooltip > * + * {
color: #e2701a;
}
.plansWrapper {
height: 50%;
position: relative;
padding: 0px 20px;
}
.plansTitle {
padding: 10px 20px;
padding-bottom: 0;
padding: 10px 0px;
color: #767e93;
font-weight: 600;
}
.overflowAuto {
overflow: auto;
.copyGenerated {
position: absolute;
bottom: 30px;
right: 60px;
cursor: pointer;
}
.copyGenerated img,
.copyExecution img {
width: 20px;
opacity: 0.6;
}
.copyGenerated img:hover,
.copyExecution img:hover {
opacity: 1;
}
.copyGenerated:focus,
.copyExecution:focus {
outline: none;
}
.codeBlock {
padding: 10px 20px;
width: 90%;
/* position: relative;
padding: 10px 20px; */
background-color: rgb(253, 249, 237);
margin: 20px;
/* margin: 20px;
width: 100%; */
width: auto;
border-radius: 5px;
max-height: 150px;
max-height: calc(100% - 60px);
overflow: auto;
margin-top: 10px;
margin-top: 0px;
min-height: calc(100% - 60px);
}
.codeBlock pre {
display: block;
padding: 0px;
padding: 10px 20px;
margin: 0px;
font-size: 13px;
line-height: unset;
@ -895,6 +919,7 @@ div.CodeMirror-lint-tooltip > * + * {
border: none;
border-radius: 0;
overflow: unset;
padding-bottom: 10px;
}
.codeBlock code {
@ -905,6 +930,46 @@ div.CodeMirror-lint-tooltip > * + * {
.graphiql-container .analyse-button-wrap {
position: relative;
}
.copyTooltip {
position: relative;
display: inline-block;
}
.copyTooltip .tooltiptext {
background-color: #555;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 4px 0px;
font-size: 14px;
position: absolute;
z-index: 1000000000;
right: -21px;
bottom: 30px;
opacity: 0;
-webkit-transition: opacity 0.3s;
transition: opacity 0.3s;
display: none;
width: 57px;
}
.copyTooltip .tooltiptext::after {
content: '';
position: absolute;
top: 24px;
right: 22px;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: #555 transparent transparent transparent;
}
.copyTooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
display: block;
}
/* BASICS */
.CodeMirror {

View File

@ -22,6 +22,7 @@ class GraphiQLWrapper extends Component {
onBoardingEnabled: false,
queries: null,
supportAnalyze: false,
analyzeApiChange: false,
};
const queryFile = this.props.queryParams
? this.props.queryParams.query_file
@ -35,13 +36,17 @@ class GraphiQLWrapper extends Component {
componentDidMount() {
if (this.props.data.serverVersion) {
this.checkSemVer(this.props.data.serverVersion);
this.checkSemVer(this.props.data.serverVersion).then(() =>
this.checkNewAnalyzeVersion(this.props.data.serverVersion)
);
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.data.serverVersion !== this.props.data.serverVersion) {
this.checkSemVer(nextProps.data.serverVersion);
this.checkSemVer(nextProps.data.serverVersion).then(() =>
this.checkNewAnalyzeVersion(nextProps.data.serverVersion)
);
}
}
shouldComponentUpdate(nextProps) {
@ -60,6 +65,21 @@ class GraphiQLWrapper extends Component {
this.updateAnalyzeState(false);
console.error(e);
}
return Promise.resolve();
}
checkNewAnalyzeVersion(version) {
try {
const analyzeApiChange = semverCheck('analyzeApiChange', version);
if (analyzeApiChange) {
this.updateAnalyzeApiState(true);
} else {
this.updateAnalyzeApiState(false);
}
} catch (e) {
this.updateAnalyzeApiState(false);
console.error(e);
}
return Promise.resolve();
}
updateAnalyzeState(supportAnalyze) {
this.setState({
@ -67,10 +87,16 @@ class GraphiQLWrapper extends Component {
supportAnalyze: supportAnalyze,
});
}
updateAnalyzeApiState(analyzeApiChange) {
this.setState({
...this.state,
analyzeApiChange: analyzeApiChange,
});
}
render() {
const styles = require('../Common/Common.scss');
const { supportAnalyze } = this.state;
const { supportAnalyze, analyzeApiChange } = this.state;
const graphQLFetcher = graphQLParams => {
if (this.state.headerFocus) {
return null;
@ -84,7 +110,8 @@ class GraphiQLWrapper extends Component {
const analyzeFetcherInstance = analyzeFetcher(
this.props.data.url,
this.props.data.headers
this.props.data.headers,
analyzeApiChange
);
// let content = "fetching schema";

View File

@ -6,6 +6,7 @@ const componentsSemver = {
eventRedeliver: '1.0.0-alpha17',
sqlAnalyze: '1.0.0-alpha25',
aggregationPerm: '1.0.0-alpha26',
analyzeApiChange: '1.0.0-alpha26',
insertPrefix: '1.0.0-alpha26',
};

View File

@ -35,7 +35,7 @@ import qualified Hasura.Server.Query as RQ
data GQLExplain
= GQLExplain
{ _gqeQuery :: !GH.GraphQLRequest
, _gqeUser :: !UserInfo
, _gqeUser :: !(Maybe (Map.HashMap Text Text))
} deriving (Show, Eq)
$(J.deriveJSON (J.aesonDrop 4 J.camelCase){J.omitNothingFields=True}
@ -103,7 +103,7 @@ explainField userInfo gCtx fld =
validateHdrs hdrs = do
let receivedHdrs = userVars userInfo
forM_ hdrs $ \hdr ->
unless (Map.member hdr receivedHdrs) $
unless (isJust $ getVarVal hdr receivedHdrs) $
throw400 NotFound $ hdr <<> " header is expected but not found"
explainGQLQuery
@ -113,7 +113,7 @@ explainGQLQuery
-> GCtxMap
-> GQLExplain
-> m BL.ByteString
explainGQLQuery pool iso gCtxMap (GQLExplain query userInfo)= do
explainGQLQuery pool iso gCtxMap (GQLExplain query userVarsRaw)= do
(opTy, selSet) <- runReaderT (GV.validateGQ query) gCtx
unless (opTy == G.OperationTypeQuery) $
throw400 InvalidParams "only queries can be explained"
@ -121,6 +121,9 @@ explainGQLQuery pool iso gCtxMap (GQLExplain query userInfo)= do
plans <- liftIO (runExceptT $ runTx tx) >>= liftEither
return $ J.encode plans
where
usrVars = mkUserVars $ maybe [] Map.toList userVarsRaw
userInfo = mkUserInfo (fromMaybe adminRole $ roleFromVars usrVars) usrVars
gCtx = getGCtx (userRole userInfo) gCtxMap
runTx tx =
Q.runTx pool (iso, Nothing) $ RQ.setHeadersTx userInfo >> tx
Q.runTx pool (iso, Nothing) $
RQ.setHeadersTx (userVars userInfo) >> tx

View File

@ -62,9 +62,9 @@ buildTx userInfo gCtx fld = do
"lookup failed: opctx: " <> showName f
validateHdrs hdrs = do
let receivedHdrs = userVars userInfo
let receivedVars = userVars userInfo
forM_ hdrs $ \hdr ->
unless (Map.member hdr receivedHdrs) $
unless (isJust $ getVarVal hdr receivedVars) $
throw400 NotFound $ hdr <<> " header is expected but not found"
-- {-# SCC resolveFld #-}

View File

@ -37,4 +37,4 @@ runGQ pool isoL userInfo gCtxMap req = do
gCtx = getGCtx (userRole userInfo) gCtxMap
runTx tx =
Q.runTx pool (isoL, Nothing) $
RQ.setHeadersTx userInfo >> tx
RQ.setHeadersTx (userVars userInfo) >> tx

View File

@ -169,7 +169,7 @@ onStart serverEnv wsConn (StartMsg opId q) = catchAndIgnore $ do
(opTy, fields) <- either (withComplete . preExecErr) return $
runReaderT (validateGQ q) gCtx
let qTx = RQ.setHeadersTx userInfo >>
let qTx = RQ.setHeadersTx (userVars userInfo) >>
resolveSelSet userInfo gCtx opTy fields
case opTy of

View File

@ -27,7 +27,6 @@ import Hasura.GraphQL.Utils
import Hasura.GraphQL.Validate.Context
import Hasura.GraphQL.Validate.Types
import Hasura.RQL.Types
import Hasura.Server.Utils (duplicates)
import Hasura.SQL.Value
newtype P a = P { unP :: Maybe (Either AnnGValue a)}

View File

@ -53,7 +53,6 @@ import Hasura.RQL.DDL.Permission.Internal
import Hasura.RQL.DDL.Permission.Triggers
import Hasura.RQL.DML.Internal (onlyPositiveInt)
import Hasura.RQL.Types
import Hasura.Server.Utils (isXHasuraTxt)
import Hasura.SQL.Types
import qualified Database.PG.Query as Q
@ -133,7 +132,7 @@ buildInsPermInfo tabInfo (PermDef rn (InsPerm chk upsrt set) _) = withPathK "per
vn = buildViewName tn rn PTInsert
fetchHdr (String t) = bool Nothing (Just $ T.toLower t)
$ isXHasuraTxt t
$ isUserVar t
fetchHdr _ = Nothing
buildInsInfra :: QualifiedTable -> InsPermInfo -> Q.TxE QErr ()

View File

@ -196,7 +196,7 @@ getDependentHeaders boolExp = case boolExp of
parseOnlyString val = case val of
(String t)
| isXHasuraTxt t -> [T.toLower t]
| isUserVar t -> [T.toLower t]
| isReqUserId t -> [userIdHeader]
| otherwise -> []
_ -> []
@ -209,7 +209,7 @@ valueParser :: (MonadError QErr m) => PGColType -> Value -> m S.SQLExp
valueParser columnType = \case
-- When it is a special variable
val@(String t)
| isXHasuraTxt t -> return $ fromCurSess t
| isUserVar t -> return $ fromCurSess t
| isReqUserId t -> return $ fromCurSess userIdHeader
| otherwise -> txtRHSBuilder columnType val
-- Typical value as Aeson's value

View File

@ -243,7 +243,7 @@ toJSONableExp colTy expn
-- validate headers
validateHeaders :: (P1C m) => [T.Text] -> m ()
validateHeaders depHeaders = do
headers <- M.keys . userVars <$> askUserInfo
headers <- getVarNames . userVars <$> askUserInfo
forM_ depHeaders $ \hdr ->
unless (hdr `elem` map T.toLower headers) $
throw400 NotFound $ hdr <<> " header is expected but not found"

View File

@ -3,13 +3,22 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Hasura.RQL.Types.Permission
( RoleName(..)
, UserId(..)
, UserVars
, UserInfo(..)
, mkUserVars
, isUserVar
, getVarNames
, getVarVal
, roleFromVars
, UserInfo
, userRole
, userVars
, mkUserInfo
, adminUserInfo
, adminRole
, isAdmin
@ -24,9 +33,6 @@ import Hasura.SQL.Types
import qualified Database.PG.Query as Q
import Data.Aeson
import qualified Data.Aeson as J
import qualified Data.Aeson.Casing as J
import qualified Data.Aeson.TH as J
import Data.Hashable
import Data.Word
import Instances.TH.Lift ()
@ -53,7 +59,34 @@ isAdmin = (adminRole ==)
newtype UserId = UserId { getUserId :: Word64 }
deriving (Show, Eq, FromJSON, ToJSON)
type UserVars = (Map.HashMap T.Text T.Text)
newtype UserVars
= UserVars { unUserVars :: Map.HashMap T.Text T.Text }
deriving (Show, Eq, FromJSON, ToJSON, Hashable)
isUserVar :: T.Text -> Bool
isUserVar = T.isPrefixOf "x-hasura-" . T.toLower
roleFromVars :: UserVars -> Maybe RoleName
roleFromVars =
fmap RoleName . getVarVal userRoleVar
getVarVal :: Text -> UserVars -> Maybe Text
getVarVal k =
Map.lookup k . unUserVars
getVarNames :: UserVars -> [T.Text]
getVarNames =
Map.keys . unUserVars
mkUserVars :: [(T.Text, T.Text)] -> UserVars
mkUserVars l =
UserVars $ Map.fromList
[ (T.toLower k, v)
| (k, v) <- l, isUserVar k
]
userRoleVar :: Text
userRoleVar = "x-hasura-role"
data UserInfo
= UserInfo
@ -61,15 +94,19 @@ data UserInfo
, userVars :: !UserVars
} deriving (Show, Eq, Generic)
mkUserInfo :: RoleName -> UserVars -> UserInfo
mkUserInfo rn (UserVars v) =
UserInfo rn $ UserVars $ Map.insert userRoleVar (getRoleTxt rn) v
instance Hashable UserInfo
$(J.deriveJSON (J.aesonDrop 4 J.camelCase){J.omitNothingFields=True}
''UserInfo
)
-- $(J.deriveToJSON (J.aesonDrop 4 J.camelCase){J.omitNothingFields=True}
-- ''UserInfo
-- )
adminUserInfo :: UserInfo
adminUserInfo =
UserInfo adminRole $ Map.singleton "x-hasura-role" "admin"
mkUserInfo adminRole $ mkUserVars []
data PermType
= PTInsert

View File

@ -104,11 +104,6 @@ parseBody = do
Just jVal -> decodeValue jVal
Nothing -> throw400 InvalidJSON "invalid json"
filterHeaders :: [(T.Text, T.Text)] -> [(T.Text, T.Text)]
filterHeaders hdrs = flip filter hdrs $ \(h, _) ->
isXHasuraTxt h && (T.toLower h /= userRoleHeader)
&& (T.toLower h /= accessKeyHeader)
onlyAdmin :: Handler ()
onlyAdmin = do
uRole <- asks (userRole . hcUser)

View File

@ -29,7 +29,6 @@ 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
@ -148,14 +147,14 @@ mkUserInfoFromResp logger url statusCode respBody
throw500 "Invalid response from authorization hook"
where
getUserInfoFromHdrs rawHeaders = do
let headers = M.fromList [(T.toLower k, v) | (k, v) <- M.toList rawHeaders]
case M.lookup userRoleHeader headers of
let usrVars = mkUserVars rawHeaders
case roleFromVars usrVars of
Nothing -> do
logError
throw500 "missing x-hasura-role key in webhook response"
Just v -> do
Just rn -> do
logWebHookResp L.LevelInfo Nothing
return $ UserInfo (RoleName v) headers
return $ mkUserInfo rn usrVars
logError =
logWebHookResp L.LevelError $ Just respBody
@ -212,7 +211,7 @@ getUserInfo logger manager rawHeaders = \case
AMNoAuth -> return userInfoFromHeaders
AMAccessKey accKey unAuthRole ->
case getHeader accessKeyHeader of
case getVarVal accessKeyHeader usrVars of
Just givenAccKey -> userInfoWhenAccessKey accKey givenAccKey
Nothing -> userInfoWhenNoAccessKey unAuthRole
@ -226,20 +225,18 @@ getUserInfo logger manager rawHeaders = \case
-- when access key is absent, run the action to retrieve UserInfo, otherwise
-- accesskey override
whenAccessKeyAbsent ak action =
maybe action (userInfoWhenAccessKey ak) $ getHeader accessKeyHeader
maybe action (userInfoWhenAccessKey ak) $ getVarVal accessKeyHeader usrVars
headers =
M.fromList $ filter (T.isPrefixOf "x-hasura-" . fst) $
flip map rawHeaders $
\(hdrName, hdrVal) ->
(T.toLower $ bsToTxt $ original hdrName, bsToTxt hdrVal)
getHeader h = M.lookup h headers
usrVars =
mkUserVars
[ (T.toLower $ bsToTxt $ original hdrName, bsToTxt hdrVal)
| (hdrName, hdrVal) <- rawHeaders
]
userInfoFromHeaders =
case M.lookup userRoleHeader headers of
Just v -> UserInfo (RoleName v) headers
Nothing -> adminUserInfo
case roleFromVars usrVars of
Just rn -> mkUserInfo rn usrVars
Nothing -> mkUserInfo adminRole usrVars
userInfoWhenAccessKey key reqKey = do
when (reqKey /= getAccessKey key) $ throw401 $ "invalid " <> accessKeyHeader
@ -247,5 +244,4 @@ getUserInfo logger manager rawHeaders = \case
userInfoWhenNoAccessKey = \case
Nothing -> throw401 $ accessKeyHeader <> " required, but not found"
Just role -> return $ UserInfo role $
M.insertWith const userRoleHeader (getRoleTxt role) headers
Just role -> return $ mkUserInfo role usrVars

View File

@ -177,8 +177,7 @@ processJwt jwtCtx headers mUnAuthRole =
withoutAuthZHeader = do
unAuthRole <- maybe missingAuthzHeader return mUnAuthRole
return $ UserInfo unAuthRole
$ Map.singleton userRoleHeader $ getRoleTxt unAuthRole
return $ mkUserInfo unAuthRole $ mkUserVars []
missingAuthzHeader =
throw400 InvalidHeaders "Missing Authorization header in JWT authentication mode"
@ -220,10 +219,9 @@ processAuthZHeader jwtCtx headers authzHeader = do
metadata <- decodeJSON $ A.Object finalClaims
-- delete the x-hasura-access-key from this map, and insert x-hasura-role
let hasuraMd = Map.insert userRoleHeader (getRoleTxt role) $
Map.delete accessKeyHeader metadata
let hasuraMd = Map.delete accessKeyHeader metadata
return $ UserInfo role hasuraMd
return $ mkUserInfo role $ mkUserVars $ Map.toList hasuraMd
where
parseAuthzHeader = do

View File

@ -105,7 +105,7 @@ runQuery
runQuery pool isoL userInfo sc query = do
tx <- liftEither $ buildTxAny userInfo sc query
res <- liftIO $ runExceptT $ Q.runTx pool (isoL, Nothing) $
setHeadersTx userInfo >> tx
setHeadersTx (userVars userInfo) >> tx
liftEither res
queryNeedsReload :: RQLQuery -> Bool
@ -218,11 +218,11 @@ buildTxAny userInfo sc rq = case rq of
, finalSc
)
setHeadersTx :: UserInfo -> Q.TxE QErr ()
setHeadersTx userInfo =
setHeadersTx :: UserVars -> Q.TxE QErr ()
setHeadersTx uVars =
Q.unitQE defaultTxErrorHandler setSess () False
where
toStrictText = LT.toStrict . AT.encodeToLazyText
setSess = Q.fromText $
"SET LOCAL \"hasura.user\" = " <>
pgFmtLit (toStrictText $ userVars userInfo)
pgFmtLit (toStrictText uVars)

View File

@ -22,9 +22,6 @@ import qualified Text.Ginger as TG
import Hasura.Prelude
isXHasuraTxt :: T.Text -> Bool
isXHasuraTxt = T.isInfixOf "x-hasura-" . T.toLower
jsonHeader :: (T.Text, T.Text)
jsonHeader = ("Content-Type", "application/json; charset=utf-8")