diff --git a/docs/graphql/manual/guides/auditing-tables.rst b/docs/graphql/manual/guides/auditing-tables.rst new file mode 100644 index 00000000000..445b7bca7c4 --- /dev/null +++ b/docs/graphql/manual/guides/auditing-tables.rst @@ -0,0 +1,22 @@ +Auditing actions on tables in Postgres +====================================== + +Typically audit logging is added to some of the tables to comply with various certifications. +You may want to capture the user information (role and the session variables) for every change in Postgres that is done through graphql-engine. + +For every mutation, hasura roughly executes the following transaction: + +.. code-block:: sql + + BEGIN; + SET local "hasura.user" = '{"x-hasura-role": "role", ... various session variables}' + SQL related to the mutation + COMMIT; + +This information can therefore be captured in any trigger on the underlying tables by using the ``current_setting`` function as follows: + +.. code-block:: sql + + current_setting('hasura.user'); + +We've set up some utility functions that'll let you quickly get started with auditing in this `repo `__. diff --git a/docs/graphql/manual/guides/index.rst b/docs/graphql/manual/guides/index.rst index 7b2a23c5e54..72e983d4274 100644 --- a/docs/graphql/manual/guides/index.rst +++ b/docs/graphql/manual/guides/index.rst @@ -54,3 +54,4 @@ Articles: Sample apps Integration/migration tutorials Integrating with monitoring frameworks + Auditing tables diff --git a/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs b/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs index 70b4043cb82..8bb9d9a2f2c 100644 --- a/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs +++ b/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs @@ -210,16 +210,16 @@ valueParser :: (MonadError QErr m) => PGColType -> Value -> m S.SQLExp valueParser columnType = \case -- When it is a special variable val@(String t) - | isXHasuraTxt t -> asCurrentSetting t - | isReqUserId t -> asCurrentSetting userIdHeader - | otherwise -> txtRHSBuilder columnType val - + | isXHasuraTxt t -> return $ fromCurSess t + | isReqUserId t -> return $ fromCurSess userIdHeader + | otherwise -> txtRHSBuilder columnType val -- Typical value as Aeson's value val -> txtRHSBuilder columnType val where - asCurrentSetting hdr = return $ S.SEUnsafe $ - "current_setting('hasura." <> dropAndSnakeCase hdr - <> "')::" <> T.pack (show columnType) + curSess = S.SEUnsafe "current_setting('hasura.user')::json" + fromCurSess hdr = + S.SEOpApp (S.SQLOp "->>") [curSess, S.SELit $ T.toLower hdr] + `S.SETyAnn` (S.AnnType $ T.pack $ show columnType) -- Convert where clause into SQL BoolExp convFilterExp :: (MonadError QErr m) diff --git a/server/src-lib/Hasura/RQL/Types/Permission.hs b/server/src-lib/Hasura/RQL/Types/Permission.hs index cfa5bbd6f66..8bd4b7ddde1 100644 --- a/server/src-lib/Hasura/RQL/Types/Permission.hs +++ b/server/src-lib/Hasura/RQL/Types/Permission.hs @@ -65,7 +65,8 @@ $(J.deriveJSON (J.aesonDrop 4 J.camelCase){J.omitNothingFields=True} ) adminUserInfo :: UserInfo -adminUserInfo = UserInfo adminRole Map.empty +adminUserInfo = + UserInfo adminRole $ Map.singleton "x-hasura-role" "admin" data PermType = PTInsert diff --git a/server/src-lib/Hasura/Server/Auth.hs b/server/src-lib/Hasura/Server/Auth.hs index 3c3942e070b..9b21b175260 100644 --- a/server/src-lib/Hasura/Server/Auth.hs +++ b/server/src-lib/Hasura/Server/Auth.hs @@ -172,7 +172,7 @@ getUserInfo logger manager rawHeaders = \case userInfoFromHeaders = case M.lookup userRoleHeader headers of Just v -> UserInfo (RoleName v) headers - Nothing -> UserInfo adminRole M.empty + Nothing -> adminUserInfo userInfoWhenAccessKey key reqKey = do when (reqKey /= getAccessKey key) $ throw401 $ "invalid " <> accessKeyHeader diff --git a/server/src-lib/Hasura/Server/Query.hs b/server/src-lib/Hasura/Server/Query.hs index 1a95294db26..39b2688fc79 100644 --- a/server/src-lib/Hasura/Server/Query.hs +++ b/server/src-lib/Hasura/Server/Query.hs @@ -10,10 +10,11 @@ import Data.Aeson.Casing import Data.Aeson.TH import Language.Haskell.TH.Syntax (Lift) +import qualified Data.Aeson.Text as AT import qualified Data.ByteString.Builder as BB import qualified Data.ByteString.Lazy as BL -import qualified Data.HashMap.Strict as Map import qualified Data.Sequence as Seq +import qualified Data.Text.Lazy as LT import qualified Data.Vector as V import Hasura.Prelude @@ -25,7 +26,6 @@ import Hasura.RQL.DDL.Schema.Table import Hasura.RQL.DML.QueryTemplate import Hasura.RQL.DML.Returning (encodeJSONVector) import Hasura.RQL.Types -import Hasura.Server.Utils import Hasura.SQL.Types import qualified Database.PG.Query as Q @@ -220,9 +220,9 @@ buildTxAny userInfo sc rq = case rq of setHeadersTx :: UserInfo -> Q.TxE QErr () setHeadersTx userInfo = - forM_ hdrs $ \h -> Q.unitQE defaultTxErrorHandler (mkQ h) () False + Q.unitQE defaultTxErrorHandler setSess () False where - hdrs = Map.toList $ Map.delete accessKeyHeader - $ userHeaders userInfo - mkQ (h, v) = Q.fromText $ - "SET LOCAL hasura." <> dropAndSnakeCase h <> " = " <> pgFmtLit v + toStrictText = LT.toStrict . AT.encodeToLazyText + setSess = Q.fromText $ + "SET LOCAL \"hasura.user\" = " <> + pgFmtLit (toStrictText $ userHeaders userInfo) diff --git a/server/src-lib/Hasura/Server/Utils.hs b/server/src-lib/Hasura/Server/Utils.hs index b32e105a629..2b2d4406027 100644 --- a/server/src-lib/Hasura/Server/Utils.hs +++ b/server/src-lib/Hasura/Server/Utils.hs @@ -21,16 +21,6 @@ import qualified Text.Ginger as TG import Hasura.Prelude - -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