diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Insert.hs b/server/src-lib/Hasura/GraphQL/Resolve/Insert.hs index 31ab2e3d..ce9d30cf 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Insert.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Insert.hs @@ -29,10 +29,9 @@ import Hasura.GraphQL.Resolve.Mutation import Hasura.GraphQL.Resolve.Select import Hasura.GraphQL.Validate.Field import Hasura.GraphQL.Validate.Types -import Hasura.RQL.DML.Internal ( dmlTxErrorHandler - , convPartialSQLExp - , sessVarFromCurrentSetting - ) +import Hasura.RQL.DML.Internal (convPartialSQLExp, + dmlTxErrorHandler, + sessVarFromCurrentSetting) import Hasura.RQL.DML.Mutation import Hasura.RQL.GBoolExp (toSQLBoolExp) import Hasura.RQL.Types @@ -115,8 +114,9 @@ traverseInsObj rim (gName, annVal) defVal@(AnnInsObj cols objRels arrRels) = -- if relational insert input is 'null' then ignore -- return default value fmap (fromMaybe defVal) $ forM objM $ \obj -> do - let relName = RelName $ G.unName gName + let relNameM = RelName <$> mkNonEmptyText (G.unName gName) onConflictM = OMap.lookup "on_conflict" obj + relName <- onNothing relNameM $ throw500 "found empty GName String" dataVal <- onNothing (OMap.lookup "data" obj) $ throw500 "\"data\" object not found" relInfo <- onNothing (Map.lookup relName rim) $ @@ -280,7 +280,7 @@ validateInsert insCols objRels addCols = do forM_ objRels $ \relInfo -> do let lCols = map fst $ riMapping relInfo relName = riName relInfo - relNameTxt = getRelTxt relName + relNameTxt = relNameToTxt relName lColConflicts = lCols `intersect` (addCols <> insCols) withPathK relNameTxt $ unless (null lColConflicts) $ throwVE $ "cannot insert object relation ship " <> relName @@ -311,7 +311,7 @@ insertObjRel strfyNum role objRelIns = RelIns singleObjIns relInfo = objRelIns multiObjIns = singleToMulti singleObjIns relName = riName relInfo - relNameTxt = getRelTxt relName + relNameTxt = relNameToTxt relName mapCols = riMapping relInfo tn = riRTable relInfo allCols = _aiTableCols singleObjIns @@ -352,7 +352,7 @@ insertArrRel strfyNum role resCols arrRelIns = RelIns multiObjIns relInfo = arrRelIns colMapping = riMapping relInfo tn = riRTable relInfo - relNameTxt = getRelTxt $ riName relInfo + relNameTxt = relNameToTxt $ riName relInfo mutFlds = [("affected_rows", RR.MCount)] -- | insert an object with object and array relationships diff --git a/server/src-lib/Hasura/GraphQL/Schema.hs b/server/src-lib/Hasura/GraphQL/Schema.hs index e2e84dd3..9297615a 100644 --- a/server/src-lib/Hasura/GraphQL/Schema.hs +++ b/server/src-lib/Hasura/GraphQL/Schema.hs @@ -63,8 +63,7 @@ isValidCol :: PGCol -> Bool isValidCol = isValidName . G.Name . getPGColTxt isValidRel :: ToTxt a => RelName -> QualifiedObject a -> Bool -isValidRel rn rt = isValidName (G.Name $ getRelTxt rn) - && isValidObjectName rt +isValidRel rn rt = isValidName (mkRelName rn) && isValidObjectName rt isValidField :: FieldInfo -> Bool isValidField = \case @@ -114,10 +113,10 @@ mkColName :: PGCol -> G.Name mkColName (PGCol n) = G.Name n mkRelName :: RelName -> G.Name -mkRelName (RelName r) = G.Name r +mkRelName rn = G.Name $ relNameToTxt rn mkAggRelName :: RelName -> G.Name -mkAggRelName (RelName r) = G.Name $ r <> "_aggregate" +mkAggRelName rn = G.Name $ relNameToTxt rn <> "_aggregate" mkBoolExpName :: QualifiedTable -> G.Name mkBoolExpName tn = @@ -225,13 +224,13 @@ mkRelFld allowAgg (RelInfo rn rTy _ remTab isManual) isNullable = case rTy of ObjRel -> [objRelFld] where objRelFld = mkHsraObjFldInfo (Just "An object relationship") - (G.Name $ getRelTxt rn) Map.empty objRelTy + (mkRelName rn) Map.empty objRelTy objRelTy = bool (G.toGT $ G.toNT relTabTy) (G.toGT relTabTy) isObjRelNullable isObjRelNullable = isManual || isNullable relTabTy = mkTableTy remTab arrRelFld = - mkHsraObjFldInfo (Just "An array relationship") (G.Name $ getRelTxt rn) + mkHsraObjFldInfo (Just "An array relationship") (mkRelName rn) (fromInpValL $ mkSelArgs remTab) arrRelTy arrRelTy = G.toGT $ G.toNT $ G.toLT $ G.toNT $ mkTableTy remTab aggArrRelFld = mkHsraObjFldInfo (Just "An aggregated array relationship") @@ -533,7 +532,7 @@ mkBoolExpInp tn fields = Left (PGColInfo colName colTy _) -> mk (mkColName colName) (mkCompExpTy colTy) Right (RelInfo relName _ _ remTab _, _, _, _, _) -> - mk (G.Name $ getRelTxt relName) (mkBoolExpTy remTab) + mk (mkRelName relName) (mkBoolExpTy remTab) mkPGColInp :: PGColInfo -> InpValInfo mkPGColInp (PGColInfo colName colTy _) = @@ -937,13 +936,13 @@ mkInsInp tn insCols relInfoMap = relInps = flip map (Map.toList relInfoMap) $ \(relName, relInfo) -> - let rty = riType relInfo - remoteQT = riRTable relInfo - in case rty of - ObjRel -> InpValInfo Nothing (G.Name $ getRelTxt relName) Nothing $ - G.toGT $ mkObjInsInpTy remoteQT - ArrRel -> InpValInfo Nothing (G.Name $ getRelTxt relName) Nothing $ - G.toGT $ mkArrInsInpTy remoteQT + let remoteQT = riRTable relInfo + tyMaker = case riType relInfo of + ObjRel -> mkObjInsInpTy + ArrRel -> mkArrInsInpTy + in InpValInfo Nothing (mkRelName relName) Nothing $ + G.toGT $ tyMaker remoteQT + {- @@ -1319,7 +1318,7 @@ mkGCtxRole' tn insPermM selPermM updColsM mkFld ty = \case Left ci -> [((ty, mkColName $ pgiName ci), Left ci)] Right (ri, allowAgg, perm, lim, _) -> - let relFld = ( (ty, G.Name $ getRelTxt $ riName ri) + let relFld = ( (ty, mkRelName $ riName ri) , Right (ri, False, perm, lim) ) aggRelFld = ( (ty, mkAggRelName $ riName ri) diff --git a/server/src-lib/Hasura/RQL/DDL/EventTrigger.hs b/server/src-lib/Hasura/RQL/DDL/EventTrigger.hs index 2a04c76c..45f15bec 100644 --- a/server/src-lib/Hasura/RQL/DDL/EventTrigger.hs +++ b/server/src-lib/Hasura/RQL/DDL/EventTrigger.hs @@ -42,7 +42,7 @@ triggerTmplt = case parseGingerTmplt $(FE.embedStringFile "src-rsr/trigger.sql.j Right tmplt -> Just tmplt pgIdenTrigger:: Ops -> TriggerName -> T.Text -pgIdenTrigger op trn = pgFmtIden (qualifyTriggerName op trn) +pgIdenTrigger op trn = pgFmtIden . qualifyTriggerName op $ triggerNameToTxt trn where qualifyTriggerName op' trn' = "notify_hasura_" <> trn' <> "_" <> T.pack (show op') @@ -61,7 +61,7 @@ getTriggerSql -> Maybe T.Text getTriggerSql op trn qt allCols strfyNum spec = let globalCtx = HashMap.fromList - [ (T.pack "NAME", trn) + [ (T.pack "NAME", triggerNameToTxt trn) , (T.pack "QUALIFIED_TRIGGER_NAME", pgIdenTrigger op trn) , (T.pack "QUALIFIED_TABLE", toSQLTxt qt) ] diff --git a/server/src-lib/Hasura/RQL/DDL/Permission.hs b/server/src-lib/Hasura/RQL/DDL/Permission.hs index e16f36d8..98d2d07c 100644 --- a/server/src-lib/Hasura/RQL/DDL/Permission.hs +++ b/server/src-lib/Hasura/RQL/DDL/Permission.hs @@ -82,9 +82,9 @@ type InsPermDef = PermDef InsPerm type CreateInsPerm = CreatePerm InsPerm buildViewName :: QualifiedTable -> RoleName -> PermType -> QualifiedTable -buildViewName (QualifiedObject sn tn) (RoleName rTxt) pt = +buildViewName (QualifiedObject sn tn) rn pt = QualifiedObject hdbViewsSchema $ TableName - (rTxt <> "__" <> T.pack (show pt) <> "__" <> snTxt <> "__" <> tnTxt) + (roleNameToTxt rn <> "__" <> T.pack (show pt) <> "__" <> snTxt <> "__" <> tnTxt) where snTxt = getSchemaTxt sn tnTxt = getTableTxt tn diff --git a/server/src-lib/Hasura/RQL/DDL/QueryCollection.hs b/server/src-lib/Hasura/RQL/DDL/QueryCollection.hs index eb94b12a..c18208aa 100644 --- a/server/src-lib/Hasura/RQL/DDL/QueryCollection.hs +++ b/server/src-lib/Hasura/RQL/DDL/QueryCollection.hs @@ -34,7 +34,7 @@ addCollectionP2 (CollectionDef queryList) = withPathK "queries" $ unless (null duplicateNames) $ throw400 NotSupported $ "found duplicate query names " - <> T.intercalate ", " (map (T.dquote . unQueryName) duplicateNames) + <> T.intercalate ", " (map (T.dquote . unNonEmptyText . unQueryName) duplicateNames) where duplicateNames = duplicates $ map _lqName queryList diff --git a/server/src-lib/Hasura/RQL/DML/Internal.hs b/server/src-lib/Hasura/RQL/DML/Internal.hs index d2773fa2..3e0f475c 100644 --- a/server/src-lib/Hasura/RQL/DML/Internal.hs +++ b/server/src-lib/Hasura/RQL/DML/Internal.hs @@ -133,14 +133,13 @@ checkPermOnCol pt allowedCols pgCol = do unless (HS.member pgCol allowedCols) $ throw400 PermissionDenied $ permErrMsg roleName where - permErrMsg (RoleName "admin") = - "no such column exists : " <>> pgCol - permErrMsg roleName = - mconcat - [ "role " <>> roleName - , " does not have permission to " - , permTypeToCode pt <> " column " <>> pgCol - ] + permErrMsg roleName + | roleName == adminRole = "no such column exists : " <>> pgCol + | otherwise = mconcat + [ "role " <>> roleName + , " does not have permission to " + , permTypeToCode pt <> " column " <>> pgCol + ] binRHSBuilder :: PGColType -> Value -> DMLP1 S.SQLExp diff --git a/server/src-lib/Hasura/RQL/DML/Select/Internal.hs b/server/src-lib/Hasura/RQL/DML/Select/Internal.hs index 629c6976..ad2242cc 100644 --- a/server/src-lib/Hasura/RQL/DML/Select/Internal.hs +++ b/server/src-lib/Hasura/RQL/DML/Select/Internal.hs @@ -641,7 +641,7 @@ mkAggSelect :: AnnAggSel -> S.Select mkAggSelect annAggSel = prefixNumToAliases $ arrNodeToSelect bn extr $ S.BELit True where - aggSel = AnnRelG (RelName "root") [] annAggSel + aggSel = AnnRelG rootRelName [] annAggSel ArrNode extr _ bn = aggSelToArrNode (Iden "root") (FieldName "root") aggSel diff --git a/server/src-lib/Hasura/RQL/Types.hs b/server/src-lib/Hasura/RQL/Types.hs index 2a657f85..593d46b9 100644 --- a/server/src-lib/Hasura/RQL/Types.hs +++ b/server/src-lib/Hasura/RQL/Types.hs @@ -104,7 +104,7 @@ askTabInfoFromTrigger trn = do let tabInfos = M.elems $ scTables sc liftMaybe (err400 NotExists errMsg) $ find (isJust.M.lookup trn.tiEventTriggerInfoMap) tabInfos where - errMsg = "event trigger " <> trn <<> " does not exist" + errMsg = "event trigger " <> triggerNameToTxt trn <<> " does not exist" askEventTriggerInfo :: (QErrM m, CacheRM m) @@ -114,7 +114,7 @@ askEventTriggerInfo trn = do let etim = tiEventTriggerInfoMap ti liftMaybe (err400 NotExists errMsg) $ M.lookup trn etim where - errMsg = "event trigger " <> trn <<> " does not exist" + errMsg = "event trigger " <> triggerNameToTxt trn <<> " does not exist" askQTemplateInfo :: (P1C m) diff --git a/server/src-lib/Hasura/RQL/Types/BoolExp.hs b/server/src-lib/Hasura/RQL/Types/BoolExp.hs index 9ec1868b..4cb8de3d 100644 --- a/server/src-lib/Hasura/RQL/Types/BoolExp.hs +++ b/server/src-lib/Hasura/RQL/Types/BoolExp.hs @@ -288,7 +288,7 @@ instance ToJSON AnnBoolExpPartialSQL where , toJSON (pci, map opExpSToJSON opExps) ) AVRel ri relBoolExp -> - ( getRelTxt $ riName ri + ( relNameToTxt $ riName ri , toJSON (ri, toJSON relBoolExp) ) opExpSToJSON :: OpExpG PartialSQLExp -> Value diff --git a/server/src-lib/Hasura/RQL/Types/Common.hs b/server/src-lib/Hasura/RQL/Types/Common.hs index f2f8498a..d05330ae 100644 --- a/server/src-lib/Hasura/RQL/Types/Common.hs +++ b/server/src-lib/Hasura/RQL/Types/Common.hs @@ -1,7 +1,9 @@ module Hasura.RQL.Types.Common ( PGColInfo(..) , RelName(..) + , relNameToTxt , RelType(..) + , rootRelName , relTypeToTxt , RelInfo(..) @@ -17,6 +19,12 @@ module Hasura.RQL.Types.Common , ColVals , MutateResp(..) , ForeignKey(..) + + , NonEmptyText + , mkNonEmptyText + , unNonEmptyText + , adminText + , rootText ) where import Hasura.Prelude @@ -25,6 +33,7 @@ import Hasura.SQL.Types import Data.Aeson import Data.Aeson.Casing import Data.Aeson.TH +import Data.Aeson.Types import qualified Data.HashMap.Strict as HM import qualified Data.Text as T import qualified Database.PG.Query as Q @@ -41,15 +50,49 @@ data PGColInfo $(deriveJSON (aesonDrop 3 snakeCase) ''PGColInfo) +newtype NonEmptyText = NonEmptyText {unNonEmptyText :: T.Text} + deriving (Show, Eq, Ord, Hashable, ToJSON, ToJSONKey, Lift, Q.ToPrepArg, DQuote) + +mkNonEmptyText :: T.Text -> Maybe NonEmptyText +mkNonEmptyText "" = Nothing +mkNonEmptyText text = Just $ NonEmptyText text + +parseNonEmptyText :: T.Text -> Parser NonEmptyText +parseNonEmptyText text = case mkNonEmptyText text of + Nothing -> fail "empty string not allowed" + Just neText -> return neText + +instance FromJSON NonEmptyText where + parseJSON = withText "String" parseNonEmptyText + +instance FromJSONKey NonEmptyText where + fromJSONKey = FromJSONKeyTextParser parseNonEmptyText + +instance Q.FromCol NonEmptyText where + fromCol bs = mkNonEmptyText <$> Q.fromCol bs + >>= maybe (Left "empty string not allowed") Right + +adminText :: NonEmptyText +adminText = NonEmptyText "admin" + +rootText :: NonEmptyText +rootText = NonEmptyText "root" + newtype RelName - = RelName {getRelTxt :: T.Text} + = RelName {getRelTxt :: NonEmptyText} deriving (Show, Eq, Hashable, FromJSON, ToJSON, Q.ToPrepArg, Q.FromCol, Lift) instance IsIden RelName where - toIden (RelName r) = Iden r + toIden rn = Iden $ relNameToTxt rn instance DQuote RelName where - dquoteTxt (RelName r) = r + dquoteTxt = relNameToTxt + +rootRelName :: RelName +rootRelName = RelName rootText + +relNameToTxt :: RelName -> T.Text +relNameToTxt = unNonEmptyText . getRelTxt relTypeToTxt :: RelType -> T.Text relTypeToTxt ObjRel = "object" @@ -101,18 +144,18 @@ fromPGCol :: PGCol -> FieldName fromPGCol (PGCol c) = FieldName c fromRel :: RelName -> FieldName -fromRel (RelName r) = FieldName r +fromRel = FieldName . relNameToTxt newtype TQueryName - = TQueryName { getTQueryName :: T.Text } + = TQueryName { getTQueryName :: NonEmptyText } deriving ( Show, Eq, Hashable, FromJSONKey, ToJSONKey , FromJSON, ToJSON, Q.ToPrepArg, Q.FromCol, Lift) instance IsIden TQueryName where - toIden (TQueryName r) = Iden r + toIden (TQueryName r) = Iden $ unNonEmptyText r instance DQuote TQueryName where - dquoteTxt (TQueryName r) = r + dquoteTxt (TQueryName r) = unNonEmptyText r newtype TemplateParam = TemplateParam { getTemplateParam :: T.Text } diff --git a/server/src-lib/Hasura/RQL/Types/EventTrigger.hs b/server/src-lib/Hasura/RQL/Types/EventTrigger.hs index c13167bd..8dfc4115 100644 --- a/server/src-lib/Hasura/RQL/Types/EventTrigger.hs +++ b/server/src-lib/Hasura/RQL/Types/EventTrigger.hs @@ -2,7 +2,8 @@ module Hasura.RQL.Types.EventTrigger ( CreateEventTriggerQuery(..) , SubscribeOpSpec(..) , SubscribeColumns(..) - , TriggerName + , TriggerName(..) + , triggerNameToTxt , Ops(..) , EventId , TriggerOpsDef(..) @@ -27,15 +28,22 @@ import Data.Aeson.Casing import Data.Aeson.TH import Hasura.Prelude import Hasura.RQL.DDL.Headers +import Hasura.RQL.Types.Common (NonEmptyText (..)) import Hasura.SQL.Types import Language.Haskell.TH.Syntax (Lift) import qualified Data.ByteString.Lazy as LBS import qualified Data.Text as T +import qualified Database.PG.Query as Q import qualified Text.Regex.TDFA as TDFA -type TriggerName = T.Text -type EventId = T.Text +newtype TriggerName = TriggerName { unTriggerName :: NonEmptyText } + deriving (Show, Eq, Hashable, Lift, FromJSON, ToJSON, ToJSONKey, Q.FromCol, Q.ToPrepArg) + +triggerNameToTxt :: TriggerName -> Text +triggerNameToTxt = unNonEmptyText . unTriggerName + +type EventId = T.Text data Ops = INSERT | UPDATE | DELETE | MANUAL deriving (Show) @@ -106,7 +114,7 @@ $(deriveToJSON (aesonDrop 3 snakeCase){omitNothingFields=True} ''WebhookConfInfo data CreateEventTriggerQuery = CreateEventTriggerQuery - { cetqName :: !T.Text + { cetqName :: !TriggerName , cetqTable :: !QualifiedTable , cetqInsert :: !(Maybe SubscribeOpSpec) , cetqUpdate :: !(Maybe SubscribeOpSpec) @@ -134,7 +142,7 @@ instance FromJSON CreateEventTriggerQuery where replace <- o .:? "replace" .!= False let regex = "^[A-Za-z]+[A-Za-z0-9_\\-]*$" :: LBS.ByteString compiledRegex = TDFA.makeRegex regex :: TDFA.Regex - isMatch = TDFA.match compiledRegex (T.unpack name) + isMatch = TDFA.match compiledRegex . T.unpack $ triggerNameToTxt name if isMatch then return () else fail "only alphanumeric and underscore and hyphens allowed for name" if any isJust [insert, update, delete] || enableManual then @@ -170,7 +178,7 @@ $(deriveJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''TriggerOpsDef) data DeleteEventTriggerQuery = DeleteEventTriggerQuery - { detqName :: !T.Text + { detqName :: !TriggerName } deriving (Show, Eq, Lift) $(deriveJSON (aesonDrop 4 snakeCase){omitNothingFields=True} ''DeleteEventTriggerQuery) @@ -187,16 +195,16 @@ data EventTriggerConf $(deriveJSON (aesonDrop 3 snakeCase){omitNothingFields=True} ''EventTriggerConf) -data RedeliverEventQuery +newtype RedeliverEventQuery = RedeliverEventQuery - { rdeqEventId :: !EventId + { rdeqEventId :: EventId } deriving (Show, Eq, Lift) $(deriveJSON (aesonDrop 4 snakeCase){omitNothingFields=True} ''RedeliverEventQuery) data InvokeEventTriggerQuery = InvokeEventTriggerQuery - { ietqName :: !T.Text + { ietqName :: !TriggerName , ietqPayload :: !Value } deriving (Show, Eq, Lift) diff --git a/server/src-lib/Hasura/RQL/Types/Permission.hs b/server/src-lib/Hasura/RQL/Types/Permission.hs index 9404f121..dacc4af0 100644 --- a/server/src-lib/Hasura/RQL/Types/Permission.hs +++ b/server/src-lib/Hasura/RQL/Types/Permission.hs @@ -1,5 +1,6 @@ module Hasura.RQL.Types.Permission ( RoleName(..) + , roleNameToTxt , SessVar , SessVarVal @@ -23,6 +24,8 @@ module Hasura.RQL.Types.Permission ) where import Hasura.Prelude +import Hasura.RQL.Types.Common (NonEmptyText, adminText, mkNonEmptyText, + unNonEmptyText) import Hasura.Server.Utils (adminSecretHeader, deprecatedAccessKeyHeader, userRoleHeader) @@ -40,15 +43,18 @@ import qualified Data.Text as T import qualified PostgreSQL.Binary.Decoding as PD newtype RoleName - = RoleName {getRoleTxt :: T.Text} + = RoleName {getRoleTxt :: NonEmptyText} deriving ( Show, Eq, Hashable, FromJSONKey, ToJSONKey, FromJSON , ToJSON, Q.FromCol, Q.ToPrepArg, Lift) instance DQuote RoleName where - dquoteTxt (RoleName r) = r + dquoteTxt = roleNameToTxt + +roleNameToTxt :: RoleName -> Text +roleNameToTxt = unNonEmptyText . getRoleTxt adminRole :: RoleName -adminRole = RoleName "admin" +adminRole = RoleName adminText isAdmin :: RoleName -> Bool isAdmin = (adminRole ==) @@ -63,9 +69,10 @@ newtype UserVars isUserVar :: T.Text -> Bool isUserVar = T.isPrefixOf "x-hasura-" . T.toLower +-- returns Nothing if x-hasura-role is an empty string roleFromVars :: UserVars -> Maybe RoleName -roleFromVars = - fmap RoleName . getVarVal userRoleHeader +roleFromVars uv = + getVarVal userRoleHeader uv >>= fmap RoleName . mkNonEmptyText getVarVal :: SessVar -> UserVars -> Maybe SessVarVal getVarVal k = @@ -90,7 +97,7 @@ data UserInfo mkUserInfo :: RoleName -> UserVars -> UserInfo mkUserInfo rn (UserVars v) = - UserInfo rn $ UserVars $ Map.insert userRoleHeader (getRoleTxt rn) $ + UserInfo rn $ UserVars $ Map.insert userRoleHeader (roleNameToTxt rn) $ foldl (flip Map.delete) v [adminSecretHeader, deprecatedAccessKeyHeader] instance Hashable UserInfo @@ -102,7 +109,7 @@ instance Hashable UserInfo userInfoToList :: UserInfo -> [(Text, Text)] userInfoToList userInfo = let vars = Map.toList $ unUserVars . userVars $ userInfo - rn = getRoleTxt . userRole $ userInfo + rn = roleNameToTxt . userRole $ userInfo in (userRoleHeader, rn) : vars adminUserInfo :: UserInfo @@ -162,7 +169,7 @@ instance Show PermId where show $ mconcat [ getTableTxt tn , "." - , getRoleTxt rn + , roleNameToTxt rn , "." , T.pack $ show pType ] diff --git a/server/src-lib/Hasura/RQL/Types/QueryCollection.hs b/server/src-lib/Hasura/RQL/Types/QueryCollection.hs index 7ffbe4b7..96477eaa 100644 --- a/server/src-lib/Hasura/RQL/Types/QueryCollection.hs +++ b/server/src-lib/Hasura/RQL/Types/QueryCollection.hs @@ -2,6 +2,7 @@ module Hasura.RQL.Types.QueryCollection where import Hasura.GraphQL.Validate.Types (stripTypenames) import Hasura.Prelude +import Hasura.RQL.Types.Common (NonEmptyText) import Hasura.SQL.Types import Data.Aeson @@ -15,13 +16,13 @@ import qualified Database.PG.Query as Q import qualified Language.GraphQL.Draft.Syntax as G newtype CollectionName - = CollectionName {unCollectionName :: T.Text} + = CollectionName {unCollectionName :: NonEmptyText} deriving ( Show, Eq, Ord, Hashable, ToJSON, ToJSONKey, Lift , FromJSON, Q.FromCol, Q.ToPrepArg, DQuote ) newtype QueryName - = QueryName {unQueryName :: T.Text} + = QueryName {unQueryName :: NonEmptyText} deriving (Show, Eq, Ord, Hashable, Lift, ToJSON, ToJSONKey, FromJSON, DQuote) newtype GQLQuery diff --git a/server/src-lib/Hasura/RQL/Types/RemoteSchema.hs b/server/src-lib/Hasura/RQL/Types/RemoteSchema.hs index dc2f4d4c..bd03abf4 100644 --- a/server/src-lib/Hasura/RQL/Types/RemoteSchema.hs +++ b/server/src-lib/Hasura/RQL/Types/RemoteSchema.hs @@ -1,6 +1,7 @@ module Hasura.RQL.Types.RemoteSchema where import Hasura.Prelude +import Hasura.RQL.Types.Common (NonEmptyText) import Language.Haskell.TH.Syntax (Lift) import System.Environment (lookupEnv) @@ -19,7 +20,7 @@ type UrlFromEnv = Text newtype RemoteSchemaName = RemoteSchemaName - { unRemoteSchemaName :: Text} + { unRemoteSchemaName :: NonEmptyText } deriving ( Show, Eq, Lift, Hashable, J.ToJSON, J.ToJSONKey , J.FromJSON, Q.ToPrepArg, Q.FromCol, DQuote ) diff --git a/server/src-lib/Hasura/RQL/Types/SchemaCacheTypes.hs b/server/src-lib/Hasura/RQL/Types/SchemaCacheTypes.hs index ccc36f08..0833504a 100644 --- a/server/src-lib/Hasura/RQL/Types/SchemaCacheTypes.hs +++ b/server/src-lib/Hasura/RQL/Types/SchemaCacheTypes.hs @@ -36,18 +36,18 @@ reportSchemaObj :: SchemaObjId -> T.Text reportSchemaObj (SOTable tn) = "table " <> qualObjectToText tn reportSchemaObj (SOFunction fn) = "function " <> qualObjectToText fn reportSchemaObj (SOQTemplate qtn) = - "query-template " <> getTQueryName qtn + "query-template " <> unNonEmptyText (getTQueryName qtn) reportSchemaObj (SOTableObj tn (TOCol cn)) = "column " <> qualObjectToText tn <> "." <> getPGColTxt cn reportSchemaObj (SOTableObj tn (TORel cn)) = - "relationship " <> qualObjectToText tn <> "." <> getRelTxt cn + "relationship " <> qualObjectToText tn <> "." <> relNameToTxt cn reportSchemaObj (SOTableObj tn (TOCons cn)) = "constraint " <> qualObjectToText tn <> "." <> getConstraintTxt cn reportSchemaObj (SOTableObj tn (TOPerm rn pt)) = - "permission " <> qualObjectToText tn <> "." <> getRoleTxt rn + "permission " <> qualObjectToText tn <> "." <> roleNameToTxt rn <> "." <> permTypeToCode pt reportSchemaObj (SOTableObj tn (TOTrigger trn )) = - "event-trigger " <> qualObjectToText tn <> "." <> trn + "event-trigger " <> qualObjectToText tn <> "." <> triggerNameToTxt trn instance Show SchemaObjId where show soi = T.unpack $ reportSchemaObj soi diff --git a/server/src-lib/Hasura/Server/App.hs b/server/src-lib/Hasura/Server/App.hs index c64d45bc..502c3924 100644 --- a/server/src-lib/Hasura/Server/App.hs +++ b/server/src-lib/Hasura/Server/App.hs @@ -585,20 +585,25 @@ httpApp corsCfg serverCtx enableConsole consoleAssetsDir enableTelemetry = do mkSpockAction encodeQErr id serverCtx $ mkPostHandler $ mkAPIRespHandler gqlExplainHandler + mkTmpltName tmpltText = + onNothing (mkNonEmptyText tmpltText) $ throw400 NotSupported "template name is empty string" + enableGraphQL = isGraphQLEnabled serverCtx enableMetadata = isMetadataEnabled serverCtx enablePGDump = isPGDumpEnabled serverCtx enableConfig = isConfigEnabled serverCtx - tmpltGetOrDeleteH tmpltName = do + tmpltGetOrDeleteH tmpltText = do tmpltArgs <- tmpltArgsFromQueryParams - mkSpockAction encodeQErr id serverCtx $ mkGetHandler $ + mkSpockAction encodeQErr id serverCtx $ mkGetHandler $ do + tmpltName <- mkTmpltName tmpltText JSONResp <$> mkQTemplateAction tmpltName tmpltArgs - tmpltPutOrPostH tmpltName = do + tmpltPutOrPostH tmpltText = do tmpltArgs <- tmpltArgsFromQueryParams mkSpockAction encodeQErr id serverCtx $ mkPostHandler $ - mkAPIRespHandler $ \bodyTmpltArgs -> + mkAPIRespHandler $ \bodyTmpltArgs -> do + tmpltName <- mkTmpltName tmpltText mkQTemplateAction tmpltName $ M.union bodyTmpltArgs tmpltArgs tmpltArgsFromQueryParams = do diff --git a/server/src-lib/Hasura/Server/Auth/JWT.hs b/server/src-lib/Hasura/Server/Auth/JWT.hs index bd157b4a..4248bdbd 100644 --- a/server/src-lib/Hasura/Server/Auth/JWT.hs +++ b/server/src-lib/Hasura/Server/Auth/JWT.hs @@ -264,7 +264,7 @@ processAuthZHeader jwtCtx headers authzHeader = do getCurrentRole defaultRole = let userRoleHeaderB = CS.cs userRoleHeader mUserRole = snd <$> find (\h -> fst h == CI.mk userRoleHeaderB) headers - in maybe defaultRole (RoleName . bsToTxt) mUserRole + in maybe defaultRole RoleName $ mUserRole >>= mkNonEmptyText . bsToTxt decodeJSON val = case A.fromJSON val of A.Error e -> throw400 JWTInvalidClaims ("x-hasura-* claims: " <> T.pack e) diff --git a/server/src-lib/Hasura/Server/Init.hs b/server/src-lib/Hasura/Server/Init.hs index 459d186f..ba8966ca 100644 --- a/server/src-lib/Hasura/Server/Init.hs +++ b/server/src-lib/Hasura/Server/Init.hs @@ -21,8 +21,9 @@ import qualified Hasura.Logging as L import qualified Text.PrettyPrint.ANSI.Leijen as PP import Hasura.Prelude -import Hasura.RQL.Types (RoleName (..), - SchemaCache (..)) +import Hasura.RQL.Types ( RoleName (..) + , SchemaCache (..) + , mkNonEmptyText ) import Hasura.Server.Auth import Hasura.Server.Cors import Hasura.Server.Logging @@ -167,7 +168,9 @@ instance FromEnv AdminSecret where fromEnv = Right . AdminSecret . T.pack instance FromEnv RoleName where - fromEnv = Right . RoleName . T.pack + fromEnv string = case mkNonEmptyText (T.pack string) of + Nothing -> Left "empty string not allowed" + Just neText -> Right $ RoleName neText instance FromEnv Bool where fromEnv = parseStrAsBool @@ -817,11 +820,13 @@ jwtSecretHelp = "The JSON containing type and the JWK used for verifying. e.g: " <> "`{\"type\": \"RS256\", \"key\": \"\", \"claims_namespace\": \"\"}`" parseUnAuthRole :: Parser (Maybe RoleName) -parseUnAuthRole = optional $ - RoleName <$> strOption ( long "unauthorized-role" <> - metavar "" <> - help (snd unAuthRoleEnv) - ) +parseUnAuthRole = fmap mkRoleName $ optional $ + strOption ( long "unauthorized-role" <> + metavar "" <> + help (snd unAuthRoleEnv) + ) + where + mkRoleName mText = mText >>= (fmap RoleName . mkNonEmptyText) parseCorsConfig :: Parser (Maybe CorsConfig) parseCorsConfig = mapCC <$> disableCors <*> corsDomain diff --git a/server/tests-py/queries/v1/non_empty_text/create_event_trigger.yaml b/server/tests-py/queries/v1/non_empty_text/create_event_trigger.yaml new file mode 100644 index 00000000..5737f5b3 --- /dev/null +++ b/server/tests-py/queries/v1/non_empty_text/create_event_trigger.yaml @@ -0,0 +1,31 @@ +description: Create event trigger with role "" +url: /v1/query +status: 400 +response: + path: $.name + error: >- + empty string not allowed + code: parse-failed +query: + type: create_event_trigger + args: + name: "" + table: users + webhook: https://httpbin.org/post + insert: + columns: "*" + payload: + - username + update: + columns: + - username + - real_name + payload: "*" + delete: + columns: "*" + headers: + - name: X-Hasura-From-Val + value: myvalue + - name: X-Hasura-From-Env + value_from_env: EVENT_WEBHOOK_HEADER + replace: false diff --git a/server/tests-py/queries/v1/non_empty_text/create_insert_permission.yaml b/server/tests-py/queries/v1/non_empty_text/create_insert_permission.yaml new file mode 100644 index 00000000..3302f357 --- /dev/null +++ b/server/tests-py/queries/v1/non_empty_text/create_insert_permission.yaml @@ -0,0 +1,21 @@ +description: Create permission with role "" +url: /v1/query +status: 400 +response: + path: $.role + error: >- + empty string not allowed + code: parse-failed +query: + type: create_insert_permission + args: + table: article + role: "" + permission: + check: + author_id: X-HASURA-USER-ID + "$or": + - category: editorial + is_reviewed: false + - category: + "$neq": editorial diff --git a/server/tests-py/queries/v1/non_empty_text/create_object_relationship.yaml b/server/tests-py/queries/v1/non_empty_text/create_object_relationship.yaml new file mode 100644 index 00000000..00f46f0f --- /dev/null +++ b/server/tests-py/queries/v1/non_empty_text/create_object_relationship.yaml @@ -0,0 +1,15 @@ +description: Create object relationship with name "" +url: /v1/query +status: 400 +response: + path: $.name + error: >- + empty string not allowed + code: parse-failed +query: + type: create_object_relationship + args: + table: article + name: "" + using: + foreign_key_constraint_on: author_id diff --git a/server/tests-py/queries/v1/non_empty_text/create_query_collection.yaml b/server/tests-py/queries/v1/non_empty_text/create_query_collection.yaml new file mode 100644 index 00000000..6cb43ce2 --- /dev/null +++ b/server/tests-py/queries/v1/non_empty_text/create_query_collection.yaml @@ -0,0 +1,17 @@ +description: Create query collection with name "" +url: /v1/query +status: 400 +response: + path: $.name + error: >- + empty string not allowed + code: parse-failed +query: + type: create_query_collection + args: + name: "" + comment: an optional comment + definition: + queries: + - name: query_1 + query: query { test {id name}} diff --git a/server/tests-py/queries/v1/non_empty_text/create_query_collection_queryname.yaml b/server/tests-py/queries/v1/non_empty_text/create_query_collection_queryname.yaml new file mode 100644 index 00000000..6cb43ce2 --- /dev/null +++ b/server/tests-py/queries/v1/non_empty_text/create_query_collection_queryname.yaml @@ -0,0 +1,17 @@ +description: Create query collection with name "" +url: /v1/query +status: 400 +response: + path: $.name + error: >- + empty string not allowed + code: parse-failed +query: + type: create_query_collection + args: + name: "" + comment: an optional comment + definition: + queries: + - name: query_1 + query: query { test {id name}} diff --git a/server/tests-py/queries/v1/non_empty_text/create_remote_schema.yaml b/server/tests-py/queries/v1/non_empty_text/create_remote_schema.yaml new file mode 100644 index 00000000..8cfbf7cb --- /dev/null +++ b/server/tests-py/queries/v1/non_empty_text/create_remote_schema.yaml @@ -0,0 +1,19 @@ +description: Create remote schema with name "" +url: /v1/query +status: 400 +response: + path: $.name + error: >- + empty string not allowed + code: parse-failed +query: + type: add_remote_schema + args: + name: "" + definition: + url: https://remote-server.com/graphql + headers: + - name: X-Server-Request-From + value: Hasura + forward_client_headers: false + comment: some optional comment diff --git a/server/tests-py/test_v1_queries.py b/server/tests-py/test_v1_queries.py index 81b9ae6b..04c91f5e 100644 --- a/server/tests-py/test_v1_queries.py +++ b/server/tests-py/test_v1_queries.py @@ -582,3 +582,28 @@ class TestCreatePermission(DefaultTestQueries): @classmethod def dir(cls): return "queries/v1/permissions" + + +class TestNonEmptyText: + + def test_create_event_trigger(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + '/create_event_trigger.yaml') + + def test_create_insert_permission(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + '/create_insert_permission.yaml') + + def test_create_query_collection(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + '/create_query_collection.yaml') + + def test_create_query_collection_queryname(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + '/create_query_collection_queryname.yaml') + + def test_create_object_relationship(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + '/create_object_relationship.yaml') + + def test_create_remote_schema(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + '/create_remote_schema.yaml') + + @classmethod + def dir(cls): + return "queries/v1/non_empty_text"