mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 08:02:15 +03:00
* move user info related code to Hasura.User module
* the RFC #4120 implementation; insert permissions with admin secret
* revert back to old RoleName based schema maps
An attempt made to avoid duplication of schema contexts in types
if any role doesn't possess any admin secret specific schema
* fix compile errors in haskell test
* keep 'user_vars' for session variables in http-logs
* no-op refacto
* tests for admin only inserts
* update docs for admin only inserts
* updated CHANGELOG.md
* default behaviour when admin secret is not set
* fix x-hasura-role to X-Hasura-Role in pytests
* introduce effective timeout in actions async tests
* update docs for admin-secret not configured case
* Update docs/graphql/manual/api-reference/schema-metadata-api/permission.rst
Co-Authored-By: Marion Schleifer <marion@hasura.io>
* Apply suggestions from code review
Co-Authored-By: Marion Schleifer <marion@hasura.io>
* a complete iteration
backend insert permissions accessable via 'x-hasura-backend-privilege'
session variable
* console changes for backend-only permissions
* provide tooltip id; update labels and tooltips;
* requested changes
* requested changes
- remove className from Toggle component
- use appropriate function name (capitalizeFirstChar -> capitalize)
* use toggle props from definitelyTyped
* fix accidental commit
* Revert "introduce effective timeout in actions async tests"
This reverts commit b7a59c19d6
.
* generate complete schema for both 'default' and 'backend' sessions
* Apply suggestions from code review
Co-Authored-By: Marion Schleifer <marion@hasura.io>
* remove unnecessary import, export Toggle as is
* update session variable in tooltip
* 'x-hasura-use-backend-only-permissions' variable to switch
* update help texts
* update docs
* update docs
* update console help text
* regenerate package-lock
* serve no backend schema when backend_only: false and header set to true
- Few type name refactor as suggested by @0x777
* update CHANGELOG.md
* Update CHANGELOG.md
* Update CHANGELOG.md
* fix a merge bug where a certain entity didn't get removed
Co-authored-by: Marion Schleifer <marion@hasura.io>
Co-authored-by: Rishichandra Wawhal <rishi@hasura.io>
Co-authored-by: rikinsk <rikin.kachhia@gmail.com>
Co-authored-by: Tirumarai Selvan <tiru@hasura.io>
This commit is contained in:
parent
6f100e0009
commit
d52bfcda4e
13
CHANGELOG.md
13
CHANGELOG.md
@ -2,6 +2,18 @@
|
||||
|
||||
## Next release
|
||||
|
||||
### server: backend only insert permissions
|
||||
|
||||
Introduces optional `backend_only` (default: `false`) configuration in insert permissions
|
||||
(see [api reference](https://deploy-preview-4224--hasura-docs.netlify.com/graphql/manual/api-reference/schema-metadata-api/permission.html#insertpermission)).
|
||||
If this is set to `true`, the insert mutation is accessible to the role only if the request
|
||||
is accompanied by `x-hasura-use-backend-only-permissions` session variable whose value is set to `true` along with the `x-hasura-admin-secret` header.
|
||||
Otherwise, the behavior of the permission remains unchanged.
|
||||
|
||||
This feature is highly useful in disabling `insert_table` mutation for a role from frontend clients while still being able to access it from a Action webhook handler (with the same role).
|
||||
|
||||
(rfc #4120) (#4224)
|
||||
|
||||
### server: debugging mode for non-admin roles
|
||||
|
||||
For any errors the server sends extra information in `extensions` field under `internal` key. Till now this was only
|
||||
@ -224,7 +236,6 @@ A new CLI migrations image is introduced to account for the new migrations workf
|
||||
(close #3969) (#4145)
|
||||
|
||||
### Bug fixes and improvements
|
||||
|
||||
- server: improve performance of replace_metadata tracking many tables (fix #3802)
|
||||
- server: option to reload remote schemas in 'reload_metadata' API (fix #3792, #4117)
|
||||
- server: fix various space leaks to avoid excessive memory consumption
|
||||
|
9
console/package-lock.json
generated
9
console/package-lock.json
generated
@ -2133,6 +2133,15 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-toggle": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-toggle/-/react-toggle-4.0.2.tgz",
|
||||
"integrity": "sha512-sHqfoKFnL0YU2+OC4meNEC8Ptx9FE8/+nFeFvNcdBa6ANA8KpAzj3R9JN8GtrvlLgjKDoYgI7iILgXYcTPo2IA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-transition-group": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-2.9.2.tgz",
|
||||
|
@ -163,6 +163,7 @@
|
||||
"@types/react-redux": "^7.1.7",
|
||||
"@types/react-router": "^5.1.4",
|
||||
"@types/react-router-redux": "^5.0.18",
|
||||
"@types/react-toggle": "^4.0.2",
|
||||
"@types/redux-devtools": "^3.0.47",
|
||||
"@types/redux-devtools-dock-monitor": "^1.1.33",
|
||||
"@types/redux-devtools-log-monitor": "^1.0.34",
|
||||
|
5
console/src/components/Common/Toggle/Toggle.tsx
Normal file
5
console/src/components/Common/Toggle/Toggle.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import Toggle from 'react-toggle';
|
||||
import './Toggle.css';
|
||||
import 'react-toggle/style.css';
|
||||
|
||||
export default Toggle;
|
@ -43,12 +43,14 @@ const defaultQueryPermissions = {
|
||||
insert: {
|
||||
check: {},
|
||||
allow_upsert: true,
|
||||
backend_only: false,
|
||||
set: {},
|
||||
columns: [],
|
||||
},
|
||||
select: {
|
||||
columns: [],
|
||||
computed_fields: [],
|
||||
backend_only: false,
|
||||
filter: {},
|
||||
limit: null,
|
||||
allow_aggregations: false,
|
||||
@ -56,9 +58,11 @@ const defaultQueryPermissions = {
|
||||
update: {
|
||||
columns: [],
|
||||
filter: {},
|
||||
backend_only: false,
|
||||
set: {},
|
||||
},
|
||||
delete: {
|
||||
backend_only: false,
|
||||
filter: {},
|
||||
},
|
||||
};
|
||||
|
@ -1,11 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
import Toggle from 'react-toggle';
|
||||
import 'react-toggle/style.css';
|
||||
import '../../../Common/ReactToggle/ReactToggleOverrides.css';
|
||||
|
||||
import Toggle from '../../../Common/Toggle/Toggle';
|
||||
import { updateMigrationModeStatus } from '../../../Main/Actions';
|
||||
import { getConfirmation } from '../../../Common/utils/jsUtils';
|
||||
|
||||
|
@ -63,6 +63,7 @@ import {
|
||||
PERM_RESET_APPLY_SAME,
|
||||
PERM_SET_APPLY_SAME_PERM,
|
||||
PERM_DEL_APPLY_SAME_PERM,
|
||||
PERM_TOGGLE_BACKEND_ONLY,
|
||||
toggleField,
|
||||
toggleAllFields,
|
||||
getBasePermissionsState,
|
||||
@ -484,7 +485,19 @@ const modifyReducer = (tableName, schemas, modifyStateOrig, action) => {
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
case PERM_TOGGLE_BACKEND_ONLY:
|
||||
const pState = modifyState.permissionsState;
|
||||
const isBackendOnly =
|
||||
pState[pState.query] && pState[pState.query].backend_only;
|
||||
return {
|
||||
...modifyState,
|
||||
permissionsState: updatePermissionsState(
|
||||
modifyState.permissionsState,
|
||||
'backend_only',
|
||||
!isBackendOnly
|
||||
),
|
||||
};
|
||||
/* Preset operations */
|
||||
case DELETE_PRESET:
|
||||
const deletedSet = {
|
||||
...modifyState.permissionsState[action.data.queryType].set,
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
getCreatePermissionQuery,
|
||||
getDropPermissionQuery,
|
||||
} from '../../../Common/utils/v1QueryUtils';
|
||||
import { capitalize } from '../../../Common/utils/jsUtils';
|
||||
|
||||
export const PERM_OPEN_EDIT = 'ModifyTable/PERM_OPEN_EDIT';
|
||||
export const PERM_SET_FILTER_TYPE = 'ModifyTable/PERM_SET_FILTER_TYPE';
|
||||
@ -37,6 +38,7 @@ export const PERM_RESET_BULK_SELECT = 'ModifyTable/PERM_RESET_BULK_SELECT';
|
||||
export const PERM_RESET_APPLY_SAME = 'ModifyTable/PERM_RESET_APPLY_SAME';
|
||||
export const PERM_SET_APPLY_SAME_PERM = 'ModifyTable/PERM_SET_APPLY_SAME_PERM';
|
||||
export const PERM_DEL_APPLY_SAME_PERM = 'ModifyTable/PERM_DEL_APPLY_SAME_PERM';
|
||||
export const PERM_TOGGLE_BACKEND_ONLY = 'ModifyTable/PERM_TOGGLE_BACKEND_ONLY';
|
||||
|
||||
export const X_HASURA_CONST = 'x-hasura-';
|
||||
|
||||
@ -93,6 +95,9 @@ const permToggleAllowAggregation = checked => ({
|
||||
type: PERM_TOGGLE_ALLOW_AGGREGATION,
|
||||
data: checked,
|
||||
});
|
||||
export const permToggleBackendOnly = () => ({
|
||||
type: PERM_TOGGLE_BACKEND_ONLY,
|
||||
});
|
||||
const permToggleModifyLimit = limit => ({
|
||||
type: PERM_TOGGLE_MODIFY_LIMIT,
|
||||
data: limit,
|
||||
@ -517,6 +522,10 @@ const applySamePermissionsBulk = (tableSchema, arePermissionsModified) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const isQueryTypeBackendOnlyCompatible = queryType => {
|
||||
return queryType === 'insert';
|
||||
};
|
||||
|
||||
const copyRolePermissions = (
|
||||
fromRole,
|
||||
tableNameWithSchema,
|
||||
@ -718,9 +727,13 @@ const permChangePermissions = changeType => {
|
||||
'_table_' +
|
||||
table;
|
||||
|
||||
const requestMsg = getIngForm(changeType) + ' Permissions...';
|
||||
const requestMsg = capitalize(
|
||||
getIngForm(changeType) + ' permissions...'
|
||||
);
|
||||
const successMsg = 'Permissions ' + getEdForm(changeType);
|
||||
const errorMsg = getIngForm(changeType) + ' permissions failed';
|
||||
const errorMsg = capitalize(
|
||||
getIngForm(changeType) + ' permissions failed'
|
||||
);
|
||||
|
||||
const customOnSuccess = () => {
|
||||
if (changeType === permChangeTypes.save) {
|
||||
|
@ -28,7 +28,9 @@ import {
|
||||
permRemoveMultipleRoles,
|
||||
permSetApplySamePerm,
|
||||
permDelApplySamePerm,
|
||||
permToggleBackendOnly,
|
||||
applySamePermissionsBulk,
|
||||
isQueryTypeBackendOnlyCompatible,
|
||||
SET_PRESET_VALUE,
|
||||
DELETE_PRESET,
|
||||
X_HASURA_CONST,
|
||||
@ -42,6 +44,7 @@ import styles from '../../../Common/Permissions/PermissionStyles.scss';
|
||||
import PermissionBuilder from './PermissionBuilder/PermissionBuilder';
|
||||
import TableHeader from '../TableCommon/TableHeader';
|
||||
import CollapsibleToggle from '../../../Common/CollapsibleToggle/CollapsibleToggle';
|
||||
import Toggle from '../../../Common/Toggle/Toggle';
|
||||
import EnhancedInput from '../../../Common/InputChecker/InputChecker';
|
||||
import {
|
||||
fetchFunctionInit,
|
||||
@ -79,6 +82,7 @@ import {
|
||||
QUERY_TYPES,
|
||||
} from '../../../Common/utils/pgUtils';
|
||||
import { showErrorNotification } from '../../Common/Notification';
|
||||
import KnowMoreLink from "../../../Common/KnowMoreLink/KnowMoreLink";
|
||||
import {
|
||||
getFilterQueries,
|
||||
replaceLegacyOperators,
|
||||
@ -582,7 +586,7 @@ class Permissions extends Component {
|
||||
sectionClasses += ' ' + styles.disabled;
|
||||
}
|
||||
|
||||
const getSectionHeader = (title, toolTip, sectionStatus) => {
|
||||
const getSectionHeader = (title, toolTip, sectionStatus, knowMoreRef) => {
|
||||
let sectionStatusHtml;
|
||||
if (sectionStatus) {
|
||||
sectionStatusHtml = (
|
||||
@ -592,9 +596,18 @@ class Permissions extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
let knowMoreHtml;
|
||||
if(knowMoreRef) {
|
||||
knowMoreHtml = (
|
||||
<span className={`${styles.add_mar_left_small} ${styles.sectionStatus}`}>
|
||||
<KnowMoreLink href={knowMoreRef}/>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{addTooltip(title, toolTip)} {sectionStatusHtml}
|
||||
{addTooltip(title, toolTip)} {knowMoreHtml} {sectionStatusHtml}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1812,6 +1825,43 @@ class Permissions extends Component {
|
||||
);
|
||||
};
|
||||
|
||||
const getBackendOnlySection = () => {
|
||||
if (!isQueryTypeBackendOnlyCompatible(permissionsState.query)) {
|
||||
return null;
|
||||
}
|
||||
const tooltip = (
|
||||
<Tooltip id="tooltip-backend-only">
|
||||
When enabled, this {permissionsState.query} mutation is accessible
|
||||
only via "trusted backends"
|
||||
</Tooltip>
|
||||
);
|
||||
const isBackendOnly = !!(
|
||||
permissionsState[permissionsState.query] &&
|
||||
permissionsState[permissionsState.query].backend_only
|
||||
);
|
||||
const backendStatus = isBackendOnly ? 'enabled' : 'disabled';
|
||||
return (
|
||||
<CollapsibleToggle
|
||||
title={getSectionHeader('Backend only', tooltip, backendStatus, 'https://docs.hasura.io/1.0/graphql/manual/auth/authorization/permission-rules.html#backend-only-inserts')}
|
||||
useDefaultTitleStyle
|
||||
testId={'toggle-backend-only'}
|
||||
>
|
||||
<div
|
||||
className={`${styles.editPermsSection} ${styles.display_flex}`}
|
||||
>
|
||||
<div className={`${styles.display_flex} ${styles.add_mar_right_mid}`}>
|
||||
<Toggle
|
||||
checked={isBackendOnly}
|
||||
onChange={() => dispatch(permToggleBackendOnly())}
|
||||
icons={false}
|
||||
/>
|
||||
</div>
|
||||
<span>Allow from backends only</span>
|
||||
</div>
|
||||
</CollapsibleToggle>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id={'permission-edit-section'}
|
||||
@ -1840,6 +1890,7 @@ class Permissions extends Component {
|
||||
{/*{getUpsertSection()}*/}
|
||||
{getPresetsSection('insert')}
|
||||
{getPresetsSection('update')}
|
||||
{getBackendOnlySection()}
|
||||
{getButtonsSection()}
|
||||
{getClonePermsSection()}
|
||||
</div>
|
||||
|
@ -154,11 +154,12 @@ InsertPermission
|
||||
- false
|
||||
- :ref:`PGColumn` array (or) ``'*'``
|
||||
- Can insert into only these columns (or all when ``'*'`` is specified)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
* - backend_only
|
||||
- false
|
||||
- Boolean
|
||||
- When set to ``true`` the mutation is accessible only if ``x-hasura-use-backend-only-permissions``
|
||||
session variable exists and is set to ``true`` and request is made with ``x-hasura-admin-secret``
|
||||
set if any auth is configured
|
||||
|
||||
.. _drop_insert_permission:
|
||||
|
||||
|
@ -62,6 +62,8 @@ For ``insert`` operations or for GraphQL mutations of the type *insert*, you can
|
||||
|
||||
* :ref:`col-presets-permissions`
|
||||
|
||||
* :ref:`backend-only-permissions`
|
||||
|
||||
**Update** permissions
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
For ``update`` operations or for GraphQL mutations of the type *update*, you can configure the following:
|
||||
@ -245,3 +247,15 @@ While this is strictly not a permission configuration, defining
|
||||
removes access to it. This preset can be defined for ``insert`` and ``update`` operations. This configuration
|
||||
is also very useful to avoid sending sensitive user-information in the query and leverage session variables
|
||||
or static data instead.
|
||||
|
||||
.. _backend-only-permissions:
|
||||
|
||||
Backend only inserts
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If an insert mutation permission is marked as ``backend_only``, the mutation is accessible to the
|
||||
given role only if ``x-hasura-use-backend-only-permissions`` session variable exists and is set to ``true``
|
||||
and request is made with ``x-hasura-admin-secret`` set if any auth is configured.
|
||||
|
||||
This might be useful if you would like to hide a mutation from the public facing API but allow access to it
|
||||
via a "trusted backend".
|
||||
|
@ -240,6 +240,7 @@ library
|
||||
, Hasura.RQL.DDL.Metadata.Generator
|
||||
, Hasura.RQL.DDL.Schema
|
||||
, Hasura.EncJSON
|
||||
, Hasura.Session
|
||||
|
||||
, Data.Aeson.Ordered
|
||||
|
||||
|
@ -40,9 +40,8 @@ import Hasura.Prelude
|
||||
import Hasura.RQL.Types (CacheRWM, Code (..), HasHttpManager,
|
||||
HasSQLGenCtx, HasSystemDefined, QErr (..),
|
||||
SQLGenCtx (..), SchemaCache (..), UserInfoM,
|
||||
adminRole, adminUserInfo,
|
||||
buildSchemaCacheStrict, decodeValue,
|
||||
throw400, userRole, withPathK)
|
||||
throw400, withPathK)
|
||||
import Hasura.RQL.Types.Run
|
||||
import Hasura.Server.API.Query (requiresAdmin, runQueryM)
|
||||
import Hasura.Server.App
|
||||
@ -53,6 +52,7 @@ import Hasura.Server.Logging
|
||||
import Hasura.Server.SchemaUpdate
|
||||
import Hasura.Server.Telemetry
|
||||
import Hasura.Server.Version
|
||||
import Hasura.Session
|
||||
|
||||
|
||||
printErrExit :: (MonadIO m) => forall a . String -> m a
|
||||
@ -395,8 +395,8 @@ instance UserAuthentication AppM where
|
||||
|
||||
instance MetadataApiAuthorization AppM where
|
||||
authorizeMetadataApi query userInfo = do
|
||||
let currRole = userRole userInfo
|
||||
when (requiresAdmin query && currRole /= adminRole) $
|
||||
let currRole = _uiRole userInfo
|
||||
when (requiresAdmin query && currRole /= adminRoleName) $
|
||||
withPathK "args" $ throw400 AccessDenied errMsg
|
||||
where
|
||||
errMsg = "restricted access : admin only"
|
||||
|
@ -30,7 +30,7 @@ import qualified Database.PG.Query.Connection as Q
|
||||
import Hasura.EncJSON
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types.Error
|
||||
import Hasura.RQL.Types.Permission
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Error
|
||||
import Hasura.SQL.Types
|
||||
|
||||
@ -93,14 +93,14 @@ runLazyTx' (PGExecCtx pgPool _) = \case
|
||||
type RespTx = Q.TxE QErr EncJSON
|
||||
type LazyRespTx = LazyTx QErr EncJSON
|
||||
|
||||
setHeadersTx :: UserVars -> Q.TxE QErr ()
|
||||
setHeadersTx uVars =
|
||||
setHeadersTx :: SessionVariables -> Q.TxE QErr ()
|
||||
setHeadersTx session =
|
||||
Q.unitQE defaultTxErrorHandler setSess () False
|
||||
where
|
||||
setSess = Q.fromText $
|
||||
"SET LOCAL \"hasura.user\" = " <> toSQLTxt (sessionInfoJsonExp uVars)
|
||||
"SET LOCAL \"hasura.user\" = " <> toSQLTxt (sessionInfoJsonExp session)
|
||||
|
||||
sessionInfoJsonExp :: UserVars -> S.SQLExp
|
||||
sessionInfoJsonExp :: SessionVariables -> S.SQLExp
|
||||
sessionInfoJsonExp = S.SELit . J.encodeToStrictText
|
||||
|
||||
defaultTxErrorHandler :: Q.PGTxErr -> QErr
|
||||
@ -142,7 +142,7 @@ withUserInfo :: UserInfo -> LazyTx QErr a -> LazyTx QErr a
|
||||
withUserInfo uInfo = \case
|
||||
LTErr e -> LTErr e
|
||||
LTNoTx a -> LTNoTx a
|
||||
LTTx tx -> LTTx $ setHeadersTx (userVars uInfo) >> tx
|
||||
LTTx tx -> LTTx $ setHeadersTx (_uiSession uInfo) >> tx
|
||||
|
||||
instance Functor (LazyTx e) where
|
||||
fmap f = \case
|
||||
|
@ -3,6 +3,8 @@ module Hasura.GraphQL.Context where
|
||||
import Hasura.Prelude
|
||||
|
||||
import Data.Aeson
|
||||
import Data.Aeson.Casing
|
||||
import Data.Aeson.TH
|
||||
import Data.Has
|
||||
|
||||
import qualified Data.HashMap.Strict as Map
|
||||
@ -11,8 +13,7 @@ import qualified Language.GraphQL.Draft.Syntax as G
|
||||
|
||||
import Hasura.GraphQL.Resolve.Types
|
||||
import Hasura.GraphQL.Validate.Types
|
||||
import Hasura.RQL.Instances ()
|
||||
import Hasura.RQL.Types.Permission
|
||||
import Hasura.Session
|
||||
|
||||
-- | A /GraphQL context/, aka the final output of GraphQL schema generation. Used to both validate
|
||||
-- incoming queries and respond to introspection queries.
|
||||
@ -49,12 +50,27 @@ instance Has TypeMap GCtx where
|
||||
instance ToJSON GCtx where
|
||||
toJSON _ = String "ToJSON for GCtx is not implemented"
|
||||
|
||||
type GCtxMap = Map.HashMap RoleName GCtx
|
||||
data RoleContext a
|
||||
= RoleContext
|
||||
{ _rctxDefault :: !a -- ^ The default context for normal sessions
|
||||
, _rctxBackend :: !(Maybe a) -- ^ The context for sessions with backend privilege.
|
||||
} deriving (Show, Eq, Functor, Foldable, Traversable)
|
||||
$(deriveToJSON (aesonDrop 5 snakeCase) ''RoleContext)
|
||||
|
||||
type GCtxMap = Map.HashMap RoleName (RoleContext GCtx)
|
||||
|
||||
queryRootNamedType :: G.NamedType
|
||||
queryRootNamedType = G.NamedType "query_root"
|
||||
|
||||
mutationRootNamedType :: G.NamedType
|
||||
mutationRootNamedType = G.NamedType "mutation_root"
|
||||
|
||||
subscriptionRootNamedType :: G.NamedType
|
||||
subscriptionRootNamedType = G.NamedType "subscription_root"
|
||||
|
||||
mkQueryRootTyInfo :: [ObjFldInfo] -> ObjTyInfo
|
||||
mkQueryRootTyInfo flds =
|
||||
mkHsraObjTyInfo (Just "query root")
|
||||
(G.NamedType "query_root") Set.empty $
|
||||
mkHsraObjTyInfo (Just "query root") queryRootNamedType Set.empty $
|
||||
mapFromL _fiName $ schemaFld:typeFld:flds
|
||||
where
|
||||
schemaFld = mkHsraObjFldInfo Nothing "__schema" Map.empty $
|
||||
|
@ -23,10 +23,8 @@ module Hasura.GraphQL.Execute
|
||||
import Control.Exception (try)
|
||||
import Control.Lens
|
||||
import Data.Has
|
||||
import Data.Text.Conversions
|
||||
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Data.HashMap.Strict as Map
|
||||
import qualified Data.HashSet as Set
|
||||
import qualified Data.Text as T
|
||||
@ -50,6 +48,7 @@ import Hasura.RQL.Types
|
||||
import Hasura.Server.Utils (RequestId, mkClientHeadersForward,
|
||||
mkSetCookieHeaders)
|
||||
import Hasura.Server.Version (HasVersion)
|
||||
import Hasura.Session
|
||||
|
||||
import qualified Hasura.GraphQL.Execute.LiveQuery as EL
|
||||
import qualified Hasura.GraphQL.Execute.Plan as EP
|
||||
@ -131,7 +130,7 @@ getExecPlanPartial userInfo sc enableAL req = do
|
||||
-- check if query is in allowlist
|
||||
when enableAL checkQueryInAllowlist
|
||||
|
||||
gCtx <- flip runCacheRT sc $ getGCtx role gCtxRoleMap
|
||||
let gCtx = getGCtx (_uiBackendOnlyFieldAccess userInfo) sc roleName
|
||||
queryParts <- flip runReaderT gCtx $ VQ.getQueryParts req
|
||||
|
||||
let opDef = VQ.qpOpDef queryParts
|
||||
@ -151,12 +150,11 @@ getExecPlanPartial userInfo sc enableAL req = do
|
||||
VT.TLCustom ->
|
||||
throw500 "unexpected custom type for top level field"
|
||||
where
|
||||
role = userRole userInfo
|
||||
gCtxRoleMap = scGCtxMap sc
|
||||
roleName = _uiRole userInfo
|
||||
|
||||
checkQueryInAllowlist =
|
||||
-- only for non-admin roles
|
||||
when (role /= adminRole) $ do
|
||||
when (roleName /= adminRoleName) $ do
|
||||
let notInAllowlist =
|
||||
not $ VQ.isQueryInAllowlist (_grQuery req) (scAllowlist sc)
|
||||
when notInAllowlist $ modifyQErr modErr $ throwVE "query is not allowed"
|
||||
@ -192,9 +190,9 @@ getResolvedExecPlan
|
||||
-> m (Telem.CacheHit, ExecPlanResolved)
|
||||
getResolvedExecPlan pgExecCtx planCache userInfo sqlGenCtx
|
||||
enableAL sc scVer httpManager reqHeaders reqUnparsed = do
|
||||
planM <- liftIO $ EP.getPlan scVer (userRole userInfo)
|
||||
planM <- liftIO $ EP.getPlan scVer (_uiRole userInfo)
|
||||
opNameM queryStr planCache
|
||||
let usrVars = userVars userInfo
|
||||
let usrVars = _uiSession userInfo
|
||||
case planM of
|
||||
-- plans are only for queries and subscriptions
|
||||
Just plan -> (Telem.Hit,) . GExPHasura <$> case plan of
|
||||
@ -207,7 +205,7 @@ getResolvedExecPlan pgExecCtx planCache userInfo sqlGenCtx
|
||||
where
|
||||
GQLReq opNameM queryStr queryVars = reqUnparsed
|
||||
addPlanToCache plan =
|
||||
liftIO $ EP.addPlan scVer (userRole userInfo)
|
||||
liftIO $ EP.addPlan scVer (_uiRole userInfo)
|
||||
opNameM queryStr plan planCache
|
||||
noExistingPlan = do
|
||||
req <- toParsed reqUnparsed
|
||||
@ -272,9 +270,6 @@ getQueryOp
|
||||
getQueryOp gCtx sqlGenCtx userInfo queryReusability actionExecuter selSet =
|
||||
runE gCtx sqlGenCtx userInfo $ EQ.convertQuerySelSet queryReusability selSet actionExecuter
|
||||
|
||||
mutationRootName :: Text
|
||||
mutationRootName = "mutation_root"
|
||||
|
||||
resolveMutSelSet
|
||||
:: ( HasVersion
|
||||
, MonadError QErr m
|
||||
@ -294,7 +289,7 @@ resolveMutSelSet
|
||||
resolveMutSelSet fields = do
|
||||
aliasedTxs <- forM (toList fields) $ \fld -> do
|
||||
fldRespTx <- case VQ._fName fld of
|
||||
"__typename" -> return (return $ encJFromJValue mutationRootName, [])
|
||||
"__typename" -> return (return $ encJFromJValue mutationRootNamedType, [])
|
||||
_ -> evalReusabilityT $ GR.mutFldToTx fld
|
||||
return (G.unName $ G.unAlias $ VQ._fAlias fld, fldRespTx)
|
||||
|
||||
@ -431,5 +426,4 @@ execRemoteGQ reqId userInfo reqHdrs q rsi opDef = do
|
||||
HTTP.HttpExceptionRequest _req content -> throw500 $ T.pack . show $ content
|
||||
HTTP.InvalidUrlException _url reason -> throw500 $ T.pack . show $ reason
|
||||
|
||||
userInfoToHdrs = userInfoToList userInfo
|
||||
& map (CI.mk . unUTF8 . fromText *** unUTF8 . fromText)
|
||||
userInfoToHdrs = sessionVariablesToHeaders $ _uiSession userInfo
|
||||
|
@ -24,6 +24,7 @@ module Hasura.GraphQL.Execute.LiveQuery.Plan
|
||||
) where
|
||||
|
||||
import Hasura.Prelude
|
||||
import Hasura.Session
|
||||
|
||||
import qualified Data.Aeson.Casing as J
|
||||
import qualified Data.Aeson.Extended as J
|
||||
@ -98,7 +99,7 @@ resolveMultiplexedValue = \case
|
||||
modifying _2 (|> colVal)
|
||||
pure ["synthetic", T.pack $ show syntheticVarIndex]
|
||||
pure $ fromResVars (PGTypeScalar $ pstType colVal) varJsonPath
|
||||
GR.UVSessVar ty sessVar -> pure $ fromResVars ty ["session", T.toLower sessVar]
|
||||
GR.UVSessVar ty sessVar -> pure $ fromResVars ty ["session", sessionVariableToText sessVar]
|
||||
GR.UVSQL sqlExp -> pure sqlExp
|
||||
GR.UVSession -> pure $ fromResVars (PGTypeScalar PGJSON) ["session"]
|
||||
where
|
||||
@ -116,7 +117,7 @@ newCohortId = CohortId <$> liftIO UUID.nextRandom
|
||||
|
||||
data CohortVariables
|
||||
= CohortVariables
|
||||
{ _cvSessionVariables :: !UserVars
|
||||
{ _cvSessionVariables :: !SessionVariables
|
||||
, _cvQueryVariables :: !ValidatedQueryVariables
|
||||
, _cvSyntheticVariables :: !ValidatedSyntheticVariables
|
||||
-- ^ To allow more queries to be multiplexed together, we introduce “synthetic” variables for
|
||||
@ -261,7 +262,8 @@ buildLiveQueryPlan pgExecCtx fieldAlias astUnresolved varTypes = do
|
||||
(astResolved, (queryVariableValues, syntheticVariableValues)) <- flip runStateT mempty $
|
||||
GR.traverseQueryRootFldAST resolveMultiplexedValue astUnresolved
|
||||
let pgQuery = mkMultiplexedQuery $ GR.toPGQuery astResolved
|
||||
parameterizedPlan = ParameterizedLiveQueryPlan (userRole userInfo) fieldAlias pgQuery
|
||||
roleName = _uiRole userInfo
|
||||
parameterizedPlan = ParameterizedLiveQueryPlan roleName fieldAlias pgQuery
|
||||
|
||||
-- We need to ensure that the values provided for variables
|
||||
-- are correct according to Postgres. Without this check
|
||||
@ -269,7 +271,7 @@ buildLiveQueryPlan pgExecCtx fieldAlias astUnresolved varTypes = do
|
||||
-- subscription will take down the entire multiplexed query
|
||||
validatedQueryVars <- validateVariables pgExecCtx queryVariableValues
|
||||
validatedSyntheticVars <- validateVariables pgExecCtx (toList syntheticVariableValues)
|
||||
let cohortVariables = CohortVariables (userVars userInfo) validatedQueryVars validatedSyntheticVars
|
||||
let cohortVariables = CohortVariables (_uiSession userInfo) validatedQueryVars validatedSyntheticVars
|
||||
plan = LiveQueryPlan parameterizedPlan cohortVariables
|
||||
reusablePlan = ReusableLiveQueryPlan parameterizedPlan validatedSyntheticVars <$> varTypes
|
||||
pure (plan, reusablePlan)
|
||||
@ -277,7 +279,7 @@ buildLiveQueryPlan pgExecCtx fieldAlias astUnresolved varTypes = do
|
||||
reuseLiveQueryPlan
|
||||
:: (MonadError QErr m, MonadIO m)
|
||||
=> PGExecCtx
|
||||
-> UserVars
|
||||
-> SessionVariables
|
||||
-> Maybe GH.VariableValues
|
||||
-> ReusableLiveQueryPlan
|
||||
-> m LiveQueryPlan
|
||||
|
@ -59,7 +59,8 @@ import Hasura.EncJSON
|
||||
import Hasura.GraphQL.Execute.LiveQuery.Options
|
||||
import Hasura.GraphQL.Execute.LiveQuery.Plan
|
||||
import Hasura.GraphQL.Transport.HTTP.Protocol
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.RQL.Types.Error
|
||||
import Hasura.Session
|
||||
|
||||
-- -------------------------------------------------------------------------------------------------
|
||||
-- Subscribers
|
||||
|
@ -11,6 +11,7 @@ module Hasura.GraphQL.Execute.Plan
|
||||
) where
|
||||
|
||||
import Hasura.Prelude
|
||||
import Hasura.Session
|
||||
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.Casing as J
|
||||
|
@ -12,7 +12,6 @@ import qualified Data.ByteString.Lazy as LBS
|
||||
import qualified Data.HashMap.Strict as Map
|
||||
import qualified Data.IntMap as IntMap
|
||||
import qualified Data.TByteString as TBS
|
||||
import qualified Data.Text as T
|
||||
import qualified Database.PG.Query as Q
|
||||
import qualified Language.GraphQL.Draft.Syntax as G
|
||||
|
||||
@ -24,17 +23,19 @@ import qualified Hasura.GraphQL.Transport.HTTP.Protocol as GH
|
||||
import qualified Hasura.GraphQL.Validate as GV
|
||||
import qualified Hasura.GraphQL.Validate.Field as V
|
||||
import qualified Hasura.SQL.DML as S
|
||||
import Hasura.Server.Version (HasVersion)
|
||||
|
||||
import Hasura.EncJSON
|
||||
import Hasura.GraphQL.Context
|
||||
import Hasura.GraphQL.Resolve.Action
|
||||
import Hasura.GraphQL.Resolve.Types
|
||||
import Hasura.GraphQL.Validate.Types
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.DML.Select (asSingleRowJsonResp)
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Server.Version (HasVersion)
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
import Hasura.SQL.Value
|
||||
import Hasura.GraphQL.Resolve.Action
|
||||
|
||||
type PlanVariables = Map.HashMap G.Variable Int
|
||||
|
||||
@ -84,10 +85,10 @@ instance J.ToJSON ReusableQueryPlan where
|
||||
|
||||
withPlan
|
||||
:: (MonadError QErr m)
|
||||
=> UserVars -> PGPlan -> ReusableVariableValues -> m PreparedSql
|
||||
=> SessionVariables -> PGPlan -> ReusableVariableValues -> m PreparedSql
|
||||
withPlan usrVars (PGPlan q reqVars prepMap) annVars = do
|
||||
prepMap' <- foldM getVar prepMap (Map.toList reqVars)
|
||||
let args = withUserVars usrVars $ IntMap.elems prepMap'
|
||||
let args = withSessionVariables usrVars $ IntMap.elems prepMap'
|
||||
return $ PreparedSql q args
|
||||
where
|
||||
getVar accum (var, prepNo) = do
|
||||
@ -100,7 +101,7 @@ withPlan usrVars (PGPlan q reqVars prepMap) annVars = do
|
||||
-- turn the current plan into a transaction
|
||||
mkCurPlanTx
|
||||
:: (MonadError QErr m)
|
||||
=> UserVars
|
||||
=> SessionVariables
|
||||
-> FieldPlans
|
||||
-> m (LazyRespTx, GeneratedSqlMap)
|
||||
mkCurPlanTx usrVars fldPlans = do
|
||||
@ -109,14 +110,14 @@ mkCurPlanTx usrVars fldPlans = do
|
||||
fldResp <- case fldPlan of
|
||||
RFPRaw resp -> return $ RRRaw resp
|
||||
RFPPostgres (PGPlan q _ prepMap) -> do
|
||||
let args = withUserVars usrVars $ IntMap.elems prepMap
|
||||
let args = withSessionVariables usrVars $ IntMap.elems prepMap
|
||||
return $ RRSql $ PreparedSql q args
|
||||
return (alias, fldResp)
|
||||
|
||||
return (mkLazyRespTx resolved, mkGeneratedSqlMap resolved)
|
||||
|
||||
withUserVars :: UserVars -> [(Q.PrepArg, PGScalarValue)] -> [(Q.PrepArg, PGScalarValue)]
|
||||
withUserVars usrVars list =
|
||||
withSessionVariables :: SessionVariables -> [(Q.PrepArg, PGScalarValue)] -> [(Q.PrepArg, PGScalarValue)]
|
||||
withSessionVariables usrVars list =
|
||||
let usrVarsAsPgScalar = PGValJSON $ Q.JSON $ J.toJSON usrVars
|
||||
prepArg = Q.toPrepVal (Q.AltJ usrVars)
|
||||
in (prepArg, usrVarsAsPgScalar):list
|
||||
@ -167,7 +168,7 @@ prepareWithPlan = \case
|
||||
R.UVSessVar ty sessVar -> do
|
||||
let sessVarVal =
|
||||
S.SEOpApp (S.SQLOp "->>")
|
||||
[currentSession, S.SELit $ T.toLower sessVar]
|
||||
[currentSession, S.SELit $ sessionVariableToText sessVar]
|
||||
return $ flip S.SETyAnn (S.mkTypeAnn ty) $ case ty of
|
||||
PGTypeScalar colTy -> withConstructorFn colTy sessVarVal
|
||||
PGTypeArray _ -> sessVarVal
|
||||
@ -177,9 +178,6 @@ prepareWithPlan = \case
|
||||
where
|
||||
currentSession = S.SEPrep 1
|
||||
|
||||
queryRootName :: Text
|
||||
queryRootName = "query_root"
|
||||
|
||||
convertQuerySelSet
|
||||
:: ( MonadError QErr m
|
||||
, MonadReader r m
|
||||
@ -197,13 +195,13 @@ convertQuerySelSet
|
||||
-> QueryActionExecuter
|
||||
-> m (LazyRespTx, Maybe ReusableQueryPlan, GeneratedSqlMap)
|
||||
convertQuerySelSet initialReusability fields actionRunner = do
|
||||
usrVars <- asks (userVars . getter)
|
||||
usrVars <- asks (_uiSession . getter)
|
||||
(fldPlans, finalReusability) <- runReusabilityTWith initialReusability $
|
||||
forM (toList fields) $ \fld -> do
|
||||
fldPlan <- case V._fName fld of
|
||||
"__type" -> fldPlanFromJ <$> R.typeR fld
|
||||
"__schema" -> fldPlanFromJ <$> R.schemaR fld
|
||||
"__typename" -> pure $ fldPlanFromJ queryRootName
|
||||
"__typename" -> pure $ fldPlanFromJ queryRootNamedType
|
||||
_ -> do
|
||||
unresolvedAst <- R.queryFldToPGAST fld actionRunner
|
||||
(q, PlanningSt _ vars prepped) <- flip runStateT initPlanningSt $
|
||||
@ -218,7 +216,7 @@ convertQuerySelSet initialReusability fields actionRunner = do
|
||||
-- use the existing plan and new variables to create a pg query
|
||||
queryOpFromPlan
|
||||
:: (MonadError QErr m)
|
||||
=> UserVars
|
||||
=> SessionVariables
|
||||
-> Maybe GH.VariableValues
|
||||
-> ReusableQueryPlan
|
||||
-> m (LazyRespTx, GeneratedSqlMap)
|
||||
|
@ -12,14 +12,15 @@ import qualified Language.GraphQL.Draft.Syntax as G
|
||||
|
||||
import Hasura.EncJSON
|
||||
import Hasura.GraphQL.Context
|
||||
import Hasura.GraphQL.Resolve.Action
|
||||
import Hasura.GraphQL.Validate.Types (evalReusabilityT, runReusabilityT)
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.DML.Internal
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Server.Version (HasVersion)
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
import Hasura.SQL.Value
|
||||
import Hasura.Server.Version (HasVersion)
|
||||
import Hasura.GraphQL.Resolve.Action
|
||||
|
||||
import qualified Hasura.GraphQL.Execute as E
|
||||
import qualified Hasura.GraphQL.Execute.LiveQuery as E
|
||||
@ -68,19 +69,19 @@ resolveVal userInfo = \case
|
||||
PGTypeScalar colTy -> withConstructorFn colTy sessVarVal
|
||||
PGTypeArray _ -> sessVarVal
|
||||
RS.UVSQL sqlExp -> return sqlExp
|
||||
RS.UVSession -> pure $ sessionInfoJsonExp $ userVars userInfo
|
||||
RS.UVSession -> pure $ sessionInfoJsonExp $ _uiSession userInfo
|
||||
|
||||
getSessVarVal
|
||||
:: (MonadError QErr m)
|
||||
=> UserInfo -> SessVar -> m SessVarVal
|
||||
=> UserInfo -> SessionVariable -> m Text
|
||||
getSessVarVal userInfo sessVar =
|
||||
onNothing (getVarVal sessVar usrVars) $
|
||||
onNothing (getSessionVariableValue sessVar sessionVariables) $
|
||||
throw400 UnexpectedPayload $
|
||||
"missing required session variable for role " <> rn <<>
|
||||
" : " <> sessVar
|
||||
" : " <> sessionVariableToText sessVar
|
||||
where
|
||||
rn = userRole userInfo
|
||||
usrVars = userVars userInfo
|
||||
rn = _uiRole userInfo
|
||||
sessionVariables = _uiSession userInfo
|
||||
|
||||
explainField
|
||||
:: (MonadError QErr m, MonadTx m, HasVersion, MonadIO m)
|
||||
@ -122,6 +123,7 @@ explainGQLQuery
|
||||
-> GQLExplain
|
||||
-> m EncJSON
|
||||
explainGQLQuery pgExecCtx sc sqlGenCtx enableAL actionExecuter (GQLExplain query userVarsRaw) = do
|
||||
userInfo <- mkUserInfo UAdminSecretSent sessionVariables $ Just adminRoleName
|
||||
(execPlan, queryReusability) <- runReusabilityT $
|
||||
E.getExecPlanPartial userInfo sc enableAL query
|
||||
(gCtx, rootSelSet) <- case execPlan of
|
||||
@ -139,6 +141,5 @@ explainGQLQuery pgExecCtx sc sqlGenCtx enableAL actionExecuter (GQLExplain query
|
||||
(plan, _) <- E.getSubsOp pgExecCtx gCtx sqlGenCtx userInfo queryReusability actionExecuter rootField
|
||||
runInTx $ encJFromJValue <$> E.explainLiveQueryPlan plan
|
||||
where
|
||||
usrVars = mkUserVars $ maybe [] Map.toList userVarsRaw
|
||||
userInfo = mkUserInfo (fromMaybe adminRole $ roleFromVars usrVars) usrVars
|
||||
sessionVariables = mkSessionVariablesText $ maybe [] Map.toList userVarsRaw
|
||||
runInTx = liftEither <=< liftIO . runExceptT . runLazyTx pgExecCtx Q.ReadOnly
|
||||
|
@ -17,6 +17,7 @@ import qualified Language.GraphQL.Draft.Syntax as G
|
||||
import qualified Network.HTTP.Client as HTTP
|
||||
import qualified Network.Wreq as Wreq
|
||||
|
||||
import Hasura.GraphQL.Schema.Merge
|
||||
import Hasura.RQL.DDL.Headers (makeHeadersFromConf)
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Server.Utils (httpExceptToJSON)
|
||||
@ -119,31 +120,9 @@ mergeRemoteSchema
|
||||
=> GS.GCtxMap
|
||||
-> GS.GCtx
|
||||
-> m GS.GCtxMap
|
||||
mergeRemoteSchema ctxMap mergedRemoteGCtx = do
|
||||
res <- forM (Map.toList ctxMap) $ \(role, gCtx) -> do
|
||||
updatedGCtx <- mergeGCtx gCtx mergedRemoteGCtx
|
||||
return (role, updatedGCtx)
|
||||
return $ Map.fromList res
|
||||
|
||||
mergeGCtx
|
||||
:: (MonadError QErr m)
|
||||
=> GS.GCtx
|
||||
-> GS.GCtx
|
||||
-> m GS.GCtx
|
||||
mergeGCtx gCtx rmMergedGCtx = do
|
||||
let rmTypes = GS._gTypes rmMergedGCtx
|
||||
hsraTyMap = GS._gTypes gCtx
|
||||
GS.checkSchemaConflicts gCtx rmMergedGCtx
|
||||
let newQR = mergeQueryRoot gCtx rmMergedGCtx
|
||||
newMR = mergeMutRoot gCtx rmMergedGCtx
|
||||
newSR = mergeSubRoot gCtx rmMergedGCtx
|
||||
newTyMap = mergeTyMaps hsraTyMap rmTypes newQR newMR
|
||||
updatedGCtx = gCtx { GS._gTypes = newTyMap
|
||||
, GS._gQueryRoot = newQR
|
||||
, GS._gMutRoot = newMR
|
||||
, GS._gSubRoot = newSR
|
||||
}
|
||||
return updatedGCtx
|
||||
mergeRemoteSchema ctxMap mergedRemoteGCtx =
|
||||
flip Map.traverseWithKey ctxMap $ \_ schemaCtx ->
|
||||
for schemaCtx $ \gCtx -> mergeGCtx gCtx mergedRemoteGCtx
|
||||
|
||||
convRemoteGCtx :: GC.RemoteGCtx -> GS.GCtx
|
||||
convRemoteGCtx rmGCtx =
|
||||
@ -153,33 +132,7 @@ convRemoteGCtx rmGCtx =
|
||||
, GS._gSubRoot = GC._rgSubscriptionRoot rmGCtx
|
||||
}
|
||||
|
||||
|
||||
mergeQueryRoot :: GS.GCtx -> GS.GCtx -> VT.ObjTyInfo
|
||||
mergeQueryRoot a b = GS._gQueryRoot a <> GS._gQueryRoot b
|
||||
|
||||
mergeMutRoot :: GS.GCtx -> GS.GCtx -> Maybe VT.ObjTyInfo
|
||||
mergeMutRoot a b = GS._gMutRoot a <> GS._gMutRoot b
|
||||
|
||||
mergeSubRoot :: GS.GCtx -> GS.GCtx -> Maybe VT.ObjTyInfo
|
||||
mergeSubRoot a b = GS._gSubRoot a <> GS._gSubRoot b
|
||||
|
||||
mergeTyMaps
|
||||
:: VT.TypeMap
|
||||
-> VT.TypeMap
|
||||
-> VT.ObjTyInfo
|
||||
-> Maybe VT.ObjTyInfo
|
||||
-> VT.TypeMap
|
||||
mergeTyMaps hTyMap rmTyMap newQR newMR =
|
||||
let newTyMap = hTyMap <> rmTyMap
|
||||
newTyMap' =
|
||||
Map.insert (G.NamedType "query_root") (VT.TIObj newQR) newTyMap
|
||||
in maybe newTyMap' (\mr -> Map.insert
|
||||
(G.NamedType "mutation_root")
|
||||
(VT.TIObj mr) newTyMap') newMR
|
||||
|
||||
|
||||
-- parsing the introspection query result
|
||||
|
||||
-- | Parsing the introspection query result
|
||||
newtype FromIntrospection a
|
||||
= FromIntrospection { fromIntrospection :: a }
|
||||
deriving (Show, Eq, Generic)
|
||||
|
@ -18,6 +18,7 @@ module Hasura.GraphQL.Resolve
|
||||
) where
|
||||
|
||||
import Data.Has
|
||||
import Hasura.Session
|
||||
|
||||
import qualified Data.HashMap.Strict as Map
|
||||
import qualified Database.PG.Query as Q
|
||||
@ -77,9 +78,9 @@ toPGQuery = \case
|
||||
validateHdrs
|
||||
:: (Foldable t, QErrM m) => UserInfo -> t Text -> m ()
|
||||
validateHdrs userInfo hdrs = do
|
||||
let receivedVars = userVars userInfo
|
||||
let receivedVars = _uiSession userInfo
|
||||
forM_ hdrs $ \hdr ->
|
||||
unless (isJust $ getVarVal hdr receivedVars) $
|
||||
unless (isJust $ getSessionVariableValue (mkSessionVariable hdr) receivedVars) $
|
||||
throw400 NotFound $ hdr <<> " header is expected but not found"
|
||||
|
||||
queryFldToPGAST
|
||||
@ -128,7 +129,7 @@ queryFldToPGAST fld actionExecuter = do
|
||||
let f = case jsonAggType of
|
||||
DS.JASMultipleRows -> QRFActionExecuteList
|
||||
DS.JASSingleObject -> QRFActionExecuteObject
|
||||
f <$> actionExecuter (RA.resolveActionQuery fld ctx (userVars userInfo))
|
||||
f <$> actionExecuter (RA.resolveActionQuery fld ctx (_uiSession userInfo))
|
||||
where
|
||||
outputType = _saecOutputType ctx
|
||||
jsonAggType = RA.mkJsonAggSelect outputType
|
||||
@ -154,13 +155,14 @@ mutFldToTx fld = do
|
||||
userInfo <- asks getter
|
||||
opCtx <- getOpCtx $ V._fName fld
|
||||
let noRespHeaders = fmap (,[])
|
||||
roleName = _uiRole userInfo
|
||||
case opCtx of
|
||||
MCInsert ctx -> do
|
||||
validateHdrs userInfo (_iocHeaders ctx)
|
||||
noRespHeaders $ RI.convertInsert (userRole userInfo) (_iocTable ctx) fld
|
||||
noRespHeaders $ RI.convertInsert roleName (_iocTable ctx) fld
|
||||
MCInsertOne ctx -> do
|
||||
validateHdrs userInfo (_iocHeaders ctx)
|
||||
noRespHeaders $ RI.convertInsertOne (userRole userInfo) (_iocTable ctx) fld
|
||||
noRespHeaders $ RI.convertInsertOne roleName (_iocTable ctx) fld
|
||||
MCUpdate ctx -> do
|
||||
validateHdrs userInfo (_uocHeaders ctx)
|
||||
noRespHeaders $ RM.convertUpdate ctx fld
|
||||
@ -174,7 +176,7 @@ mutFldToTx fld = do
|
||||
validateHdrs userInfo (_docHeaders ctx)
|
||||
noRespHeaders $ RM.convertDeleteByPk ctx fld
|
||||
MCAction ctx ->
|
||||
RA.resolveActionMutation fld ctx (userVars userInfo)
|
||||
RA.resolveActionMutation fld ctx (_uiSession userInfo)
|
||||
|
||||
getOpCtx
|
||||
:: ( MonadReusability m
|
||||
|
@ -52,6 +52,7 @@ import Hasura.RQL.Types
|
||||
import Hasura.RQL.Types.Run
|
||||
import Hasura.Server.Utils (mkClientHeadersForward, mkSetCookieHeaders)
|
||||
import Hasura.Server.Version (HasVersion)
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
import Hasura.SQL.Value (PGScalarValue (..), pgScalarValueToJson,
|
||||
toTxtValue)
|
||||
@ -64,7 +65,7 @@ $(J.deriveJSON (J.aesonDrop 3 J.snakeCase) ''ActionContext)
|
||||
data ActionWebhookPayload
|
||||
= ActionWebhookPayload
|
||||
{ _awpAction :: !ActionContext
|
||||
, _awpSessionVariables :: !UserVars
|
||||
, _awpSessionVariables :: !SessionVariables
|
||||
, _awpInput :: !J.Value
|
||||
} deriving (Show, Eq)
|
||||
$(J.deriveJSON (J.aesonDrop 4 J.snakeCase) ''ActionWebhookPayload)
|
||||
@ -129,7 +130,7 @@ resolveActionMutation
|
||||
)
|
||||
=> Field
|
||||
-> ActionMutationExecutionContext
|
||||
-> UserVars
|
||||
-> SessionVariables
|
||||
-> m (RespTx, HTTP.ResponseHeaders)
|
||||
resolveActionMutation field executionContext sessionVariables =
|
||||
case executionContext of
|
||||
@ -153,7 +154,7 @@ resolveActionMutationSync
|
||||
)
|
||||
=> Field
|
||||
-> ActionExecutionContext
|
||||
-> UserVars
|
||||
-> SessionVariables
|
||||
-> m (RespTx, HTTP.ResponseHeaders)
|
||||
resolveActionMutationSync field executionContext sessionVariables = do
|
||||
let inputArgs = J.toJSON $ fmap annInpValueToJson $ _fArguments field
|
||||
@ -205,7 +206,7 @@ resolveActionQuery
|
||||
)
|
||||
=> Field
|
||||
-> ActionExecutionContext
|
||||
-> UserVars
|
||||
-> SessionVariables
|
||||
-> HTTP.Manager
|
||||
-> [HTTP.Header]
|
||||
-> m (RS.AnnSimpleSelG UnresolvedVal)
|
||||
@ -244,7 +245,7 @@ resolveActionMutationAsync
|
||||
, Has [HTTP.Header] r
|
||||
)
|
||||
=> Field
|
||||
-> UserVars
|
||||
-> SessionVariables
|
||||
-> m RespTx
|
||||
resolveActionMutationAsync field sessionVariables = do
|
||||
reqHeaders <- asks getter
|
||||
@ -333,13 +334,13 @@ resolveAsyncActionQuery userInfo selectOpCtx field = do
|
||||
sessionVarsColumnInfo = PGColumnInfo (unsafePGCol "session_variables") "session_variables"
|
||||
0 (PGColumnScalar PGJSONB) False Nothing
|
||||
sessionVarValue = UVPG $ AnnPGVal Nothing False $ WithScalarType PGJSONB
|
||||
$ PGValJSONB $ Q.JSONB $ J.toJSON $ userVars userInfo
|
||||
$ PGValJSONB $ Q.JSONB $ J.toJSON $ _uiSession userInfo
|
||||
sessionVarsColumnEq = BoolFld $ AVCol sessionVarsColumnInfo [AEQ True sessionVarValue]
|
||||
|
||||
-- For non-admin roles, accessing an async action's response should be allowed only for the user
|
||||
-- who initiated the action through mutation. The action's response is accessible for a query/subscription
|
||||
-- only when it's session variables are equal to that of action's.
|
||||
in if isAdmin (userRole userInfo) then actionIdColumnEq
|
||||
in if isAdmin (_uiRole userInfo) then actionIdColumnEq
|
||||
else BoolAnd [actionIdColumnEq, sessionVarsColumnEq]
|
||||
|
||||
data ActionLogItem
|
||||
@ -347,7 +348,7 @@ data ActionLogItem
|
||||
{ _aliId :: !UUID.UUID
|
||||
, _aliActionName :: !ActionName
|
||||
, _aliRequestHeaders :: ![HTTP.Header]
|
||||
, _aliSessionVariables :: !UserVars
|
||||
, _aliSessionVariables :: !SessionVariables
|
||||
, _aliInputPayload :: !J.Value
|
||||
} deriving (Show, Eq)
|
||||
|
||||
|
@ -8,6 +8,7 @@ import Control.Arrow ((>>>))
|
||||
import Data.Has
|
||||
import Hasura.EncJSON
|
||||
import Hasura.Prelude
|
||||
import Hasura.Session
|
||||
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.Casing as J
|
||||
@ -455,7 +456,7 @@ insertMultipleObjects strfyNum role tn multiObjIns addCols mutOutput errP =
|
||||
insertObj strfyNum role tn objIns addCols
|
||||
|
||||
let affRows = sum $ map fst insResps
|
||||
columnValues = catMaybes $ map snd insResps
|
||||
columnValues = mapMaybe snd insResps
|
||||
cteExp <- mkSelCTEFromColVals tn tableColInfos columnValues
|
||||
let sql = toSQL $ RR.mkMutationOutputExp tn tableColInfos (Just affRows) cteExp mutOutput strfyNum
|
||||
runIdentity . Q.getRow
|
||||
|
@ -12,6 +12,7 @@ import qualified Data.HashSet as Set
|
||||
import qualified Data.Text as T
|
||||
import qualified Language.GraphQL.Draft.Syntax as G
|
||||
|
||||
import Hasura.GraphQL.Context
|
||||
import Hasura.GraphQL.Resolve.Context
|
||||
import Hasura.GraphQL.Resolve.InputValue
|
||||
import Hasura.GraphQL.Validate.Context
|
||||
@ -330,9 +331,9 @@ schemaR fld =
|
||||
"__typename" -> retJT "__Schema"
|
||||
"types" -> fmap J.toJSON $ mapM (namedTypeR' subFld) $
|
||||
sortOn getNamedTy $ Map.elems tyMap
|
||||
"queryType" -> J.toJSON <$> namedTypeR (G.NamedType "query_root") subFld
|
||||
"mutationType" -> typeR' "mutation_root" subFld
|
||||
"subscriptionType" -> typeR' "subscription_root" subFld
|
||||
"queryType" -> J.toJSON <$> namedTypeR queryRootNamedType subFld
|
||||
"mutationType" -> typeR' mutationRootNamedType subFld
|
||||
"subscriptionType" -> typeR' subscriptionRootNamedType subFld
|
||||
"directives" -> J.toJSON <$> mapM (directiveR subFld)
|
||||
(sortOn _diName defaultDirectives)
|
||||
_ -> return J.Null
|
||||
@ -342,15 +343,15 @@ typeR
|
||||
=> Field -> m J.Value
|
||||
typeR fld = do
|
||||
name <- asPGColText =<< getArg args "name"
|
||||
typeR' (G.Name name) fld
|
||||
typeR' (G.NamedType $ G.Name name) fld
|
||||
where
|
||||
args = _fArguments fld
|
||||
|
||||
typeR'
|
||||
:: (MonadReader r m, Has TypeMap r, MonadError QErr m)
|
||||
=> G.Name -> Field -> m J.Value
|
||||
=> G.NamedType -> Field -> m J.Value
|
||||
typeR' n fld = do
|
||||
tyMap <- asks getter
|
||||
case Map.lookup (G.NamedType n) tyMap of
|
||||
case Map.lookup n tyMap of
|
||||
Nothing -> return J.Null
|
||||
Just tyInfo -> J.Object <$> namedTypeR' fld tyInfo
|
||||
|
@ -21,7 +21,7 @@ import Hasura.RQL.Types.Common
|
||||
import Hasura.RQL.Types.ComputedField
|
||||
import Hasura.RQL.Types.CustomTypes
|
||||
import Hasura.RQL.Types.Function
|
||||
import Hasura.RQL.Types.Permission
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
import Hasura.SQL.Value
|
||||
|
||||
@ -231,7 +231,7 @@ partialSQLExpToUnresolvedVal = \case
|
||||
data UnresolvedVal
|
||||
-- | an entire session variables JSON object
|
||||
= UVSession
|
||||
| UVSessVar !(PGType PGScalarType) !SessVar
|
||||
| UVSessVar !(PGType PGScalarType) !SessionVariable
|
||||
-- | a SQL value literal that can be parameterized over
|
||||
| UVPG !AnnPGVal
|
||||
-- | an arbitrary SQL expression, which /cannot/ be parameterized over
|
||||
|
@ -32,6 +32,7 @@ import Hasura.Prelude
|
||||
import Hasura.RQL.DML.Internal (mkAdminRolePermInfo)
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Server.Utils (duplicates)
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
|
||||
import Hasura.GraphQL.Schema.Action
|
||||
@ -47,10 +48,12 @@ import Hasura.GraphQL.Schema.Mutation.Update
|
||||
import Hasura.GraphQL.Schema.OrderBy
|
||||
import Hasura.GraphQL.Schema.Select
|
||||
|
||||
type TableSchemaCtx = RoleContext (TyAgg, RootFields, InsCtxMap)
|
||||
|
||||
getInsPerm :: TableInfo -> RoleName -> Maybe InsPermInfo
|
||||
getInsPerm tabInfo role
|
||||
| role == adminRole = _permIns $ mkAdminRolePermInfo (_tiCoreInfo tabInfo)
|
||||
| otherwise = Map.lookup role rolePermInfoMap >>= _permIns
|
||||
getInsPerm tabInfo roleName
|
||||
| roleName == adminRoleName = _permIns $ mkAdminRolePermInfo (_tiCoreInfo tabInfo)
|
||||
| otherwise = Map.lookup roleName rolePermInfoMap >>= _permIns
|
||||
where
|
||||
rolePermInfoMap = _tiRolePermInfoMap tabInfo
|
||||
|
||||
@ -116,6 +119,7 @@ mkComputedFieldFunctionArgSeq inputArgs =
|
||||
Seq.fromList $ procFuncArgs inputArgs faName $
|
||||
\fa t -> FunctionArgItem (G.Name t) (faName fa) (faHasDefault fa)
|
||||
|
||||
-- see Note [Split schema generation (TODO)]
|
||||
mkGCtxRole'
|
||||
:: QualifiedTable
|
||||
-> Maybe PGDescription
|
||||
@ -321,6 +325,7 @@ mkGCtxRole' tn descM insPermM selPermM updColsM delPermM pkeyCols constraints vi
|
||||
computedFieldFuncArgsInps = map (TIInpObj . fst) computedFieldReqTypes
|
||||
computedFieldFuncArgScalars = Set.fromList $ concatMap snd computedFieldReqTypes
|
||||
|
||||
-- see Note [Split schema generation (TODO)]
|
||||
getRootFldsRole'
|
||||
:: QualifiedTable
|
||||
-> Maybe (PrimaryKey PGColumnInfo)
|
||||
@ -455,8 +460,8 @@ getRootFldsRole' tn primaryKey constraints fields funcs insM
|
||||
|
||||
|
||||
getSelPermission :: TableInfo -> RoleName -> Maybe SelPermInfo
|
||||
getSelPermission tabInfo role =
|
||||
Map.lookup role (_tiRolePermInfoMap tabInfo) >>= _permSel
|
||||
getSelPermission tabInfo roleName =
|
||||
Map.lookup roleName (_tiRolePermInfoMap tabInfo) >>= _permSel
|
||||
|
||||
getSelPerm
|
||||
:: (MonadError QErr m)
|
||||
@ -466,11 +471,11 @@ getSelPerm
|
||||
-- role and its permission
|
||||
-> RoleName -> SelPermInfo
|
||||
-> m (Bool, [SelField])
|
||||
getSelPerm tableCache fields role selPermInfo = do
|
||||
getSelPerm tableCache fields roleName selPermInfo = do
|
||||
|
||||
relFlds <- fmap catMaybes $ forM validRels $ \relInfo -> do
|
||||
remTableInfo <- getTabInfo tableCache $ riRTable relInfo
|
||||
let remTableSelPermM = getSelPermission remTableInfo role
|
||||
let remTableSelPermM = getSelPermission remTableInfo roleName
|
||||
remTableFlds = _tciFieldInfoMap $ _tiCoreInfo remTableInfo
|
||||
remTableColGNameMap =
|
||||
mkPGColGNameMap $ getValidCols remTableFlds
|
||||
@ -492,7 +497,7 @@ getSelPerm tableCache fields role selPermInfo = do
|
||||
CFRScalar scalarTy -> pure $ Just $ CFTScalar scalarTy
|
||||
CFRSetofTable retTable -> do
|
||||
retTableInfo <- getTabInfo tableCache retTable
|
||||
let retTableSelPermM = getSelPermission retTableInfo role
|
||||
let retTableSelPermM = getSelPermission retTableInfo roleName
|
||||
retTableFlds = _tciFieldInfoMap $ _tiCoreInfo retTableInfo
|
||||
retTableColGNameMap =
|
||||
mkPGColGNameMap $ getValidCols retTableFlds
|
||||
@ -690,34 +695,66 @@ mkGCtxMapTable
|
||||
=> TableCache
|
||||
-> FunctionCache
|
||||
-> TableInfo
|
||||
-> m (Map.HashMap RoleName (TyAgg, RootFields, InsCtxMap))
|
||||
-> m (Map.HashMap RoleName TableSchemaCtx)
|
||||
mkGCtxMapTable tableCache funcCache tabInfo = do
|
||||
m <- flip Map.traverseWithKey rolePerms $
|
||||
mkGCtxRole tableCache tn descM fields primaryKey validConstraints
|
||||
tabFuncs viewInfo customConfig
|
||||
m <- flip Map.traverseWithKey rolePermsMap $ \roleName rolePerm ->
|
||||
for rolePerm $ mkGCtxRole tableCache tn descM fields primaryKey validConstraints
|
||||
tabFuncs viewInfo customConfig roleName
|
||||
adminInsCtx <- mkAdminInsCtx tableCache fields
|
||||
adminSelFlds <- mkAdminSelFlds fields tableCache
|
||||
let adminCtx = mkGCtxRole' tn descM (Just (cols, icRelations adminInsCtx))
|
||||
(Just (True, adminSelFlds)) (Just cols) (Just ())
|
||||
primaryKey validConstraints viewInfo tabFuncs
|
||||
adminInsCtxMap = Map.singleton tn adminInsCtx
|
||||
return $ Map.insert adminRole (adminCtx, adminRootFlds, adminInsCtxMap) m
|
||||
adminTableCtx = RoleContext (adminCtx, adminRootFlds, adminInsCtxMap) Nothing
|
||||
pure $ Map.insert adminRoleName adminTableCtx m
|
||||
where
|
||||
TableInfo coreInfo rolePerms _ = tabInfo
|
||||
TableCoreInfo tn descM _ fields primaryKey _ _ viewInfo _ customConfig = coreInfo
|
||||
validConstraints = mkValidConstraints $ map _cName (tciUniqueOrPrimaryKeyConstraints coreInfo)
|
||||
cols = getValidCols fields
|
||||
tabFuncs = filter (isValidObjectName . fiName) $
|
||||
getFuncsOfTable tn funcCache
|
||||
tabFuncs = filter (isValidObjectName . fiName) $ getFuncsOfTable tn funcCache
|
||||
|
||||
adminRootFlds =
|
||||
getRootFldsRole' tn primaryKey validConstraints fields tabFuncs
|
||||
(Just ([], True)) (Just (noFilter, Nothing, [], True))
|
||||
(Just (cols, mempty, noFilter, Nothing, [])) (Just (noFilter, []))
|
||||
viewInfo customConfig
|
||||
|
||||
rolePermsMap :: Map.HashMap RoleName (RoleContext RolePermInfo)
|
||||
rolePermsMap = flip Map.map rolePerms $ \permInfo ->
|
||||
case _permIns permInfo of
|
||||
Nothing -> RoleContext permInfo Nothing
|
||||
Just insPerm ->
|
||||
if ipiBackendOnly insPerm then
|
||||
-- Remove insert permission from 'default' context and keep it in 'backend' context.
|
||||
RoleContext { _rctxDefault = permInfo{_permIns = Nothing}
|
||||
, _rctxBackend = Just permInfo
|
||||
}
|
||||
-- Remove insert permission from 'backend' context and keep it in 'default' context.
|
||||
else RoleContext { _rctxDefault = permInfo
|
||||
, _rctxBackend = Just permInfo{_permIns = Nothing}
|
||||
}
|
||||
|
||||
noFilter :: AnnBoolExpPartialSQL
|
||||
noFilter = annBoolExpTrue
|
||||
|
||||
{- Note [Split schema generation (TODO)]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
As of writing this, the schema is generated per table per role and for all permissions.
|
||||
See functions "mkGCtxRole'" and "getRootFldsRole'". This approach makes hard to
|
||||
differentiate schema generation for each operation (select, insert, delete and update)
|
||||
based on respective permission information and safe merging those schemas eventually.
|
||||
For backend-only inserts (see https://github.com/hasura/graphql-engine/pull/4224)
|
||||
we need to somehow defer the logic of merging schema for inserts with others based on its
|
||||
backend-only credibility. This requires significant refactor of this module and
|
||||
we can't afford to do it as of now since we're going to rewrite the entire GraphQL schema
|
||||
generation (see https://github.com/hasura/graphql-engine/pull/4111). For aforementioned
|
||||
backend-only inserts, we're following a hacky implementation of generating schema for
|
||||
both default session and one with backend privilege -- the later differs with the former by
|
||||
only having the schema related to insert operation.
|
||||
-}
|
||||
|
||||
mkGCtxMap
|
||||
:: forall m. (MonadError QErr m)
|
||||
=> TableCache -> FunctionCache -> ActionCache -> m GCtxMap
|
||||
@ -727,47 +764,71 @@ mkGCtxMap tableCache functionCache actionCache = do
|
||||
let actionsSchema = mkActionsSchema actionCache
|
||||
typesMap <- combineTypes actionsSchema typesMapL
|
||||
let gCtxMap = flip Map.map typesMap $
|
||||
\(ty, flds, insCtxMap) -> mkGCtx ty flds insCtxMap
|
||||
return gCtxMap
|
||||
fmap (\(ty, flds, insCtxMap) -> mkGCtx ty flds insCtxMap)
|
||||
pure gCtxMap
|
||||
where
|
||||
tableFltr ti = not (isSystemDefined $ _tciSystemDefined ti) && isValidObjectName (_tciName ti)
|
||||
|
||||
combineTypes
|
||||
:: Map.HashMap RoleName (RootFields, TyAgg)
|
||||
-> [Map.HashMap RoleName (TyAgg, RootFields, InsCtxMap)]
|
||||
-> m (Map.HashMap RoleName (TyAgg, RootFields, InsCtxMap))
|
||||
combineTypes actionsSchema maps = do
|
||||
let listMap = foldr (Map.unionWith (++) . Map.map pure)
|
||||
((\(rf, tyAgg) -> pure (tyAgg, rf, mempty)) <$> actionsSchema)
|
||||
maps
|
||||
flip Map.traverseWithKey listMap $ \_ typeList -> do
|
||||
let tyAgg = mconcat $ map (^. _1) typeList
|
||||
insCtx = mconcat $ map (^. _3) typeList
|
||||
rootFields <- combineRootFields $ map (^. _2) typeList
|
||||
pure (tyAgg, rootFields, insCtx)
|
||||
-> [Map.HashMap RoleName TableSchemaCtx]
|
||||
-> m (Map.HashMap RoleName TableSchemaCtx)
|
||||
combineTypes actionsSchema tableCtxMaps = do
|
||||
let tableCtxsMap =
|
||||
foldr (Map.unionWith (++) . Map.map pure)
|
||||
((\(rf, tyAgg) -> pure $ RoleContext (tyAgg, rf, mempty) Nothing) <$> actionsSchema)
|
||||
tableCtxMaps
|
||||
|
||||
combineRootFields :: [RootFields] -> m RootFields
|
||||
combineRootFields rootFields = do
|
||||
let duplicateQueryFields = duplicates $
|
||||
concatMap (Map.keys . _rootQueryFields) rootFields
|
||||
duplicateMutationFields = duplicates $
|
||||
concatMap (Map.keys . _rootMutationFields) rootFields
|
||||
flip Map.traverseWithKey tableCtxsMap $ \_ tableSchemaCtxs -> do
|
||||
let defaultTableSchemaCtxs = map _rctxDefault tableSchemaCtxs
|
||||
backendGCtxTypesMaybe =
|
||||
-- If no table has 'backend' schema context then
|
||||
-- aggregated context should be Nothing
|
||||
if all (isNothing . _rctxBackend) tableSchemaCtxs then Nothing
|
||||
else Just $ flip map tableSchemaCtxs $
|
||||
-- Consider 'default' if 'backend' doesn't exist for any table
|
||||
-- see Note [Split schema generation (TODO)]
|
||||
\(RoleContext def backend) -> fromMaybe def backend
|
||||
|
||||
-- TODO: The following exception should result in inconsistency
|
||||
when (not $ null duplicateQueryFields) $
|
||||
throw400 Unexpected $ "following query root fields are duplicated: "
|
||||
<> showNames duplicateQueryFields
|
||||
RoleContext <$> combineTypes' defaultTableSchemaCtxs
|
||||
<*> mapM combineTypes' backendGCtxTypesMaybe
|
||||
where
|
||||
combineTypes' :: [(TyAgg, RootFields, InsCtxMap)] -> m (TyAgg, RootFields, InsCtxMap)
|
||||
combineTypes' typeList = do
|
||||
let tyAgg = mconcat $ map (^. _1) typeList
|
||||
insCtx = mconcat $ map (^. _3) typeList
|
||||
rootFields <- combineRootFields $ map (^. _2) typeList
|
||||
pure (tyAgg, rootFields, insCtx)
|
||||
|
||||
when (not $ null duplicateMutationFields) $
|
||||
throw400 Unexpected $ "following mutation root fields are duplicated: "
|
||||
<> showNames duplicateMutationFields
|
||||
combineRootFields :: [RootFields] -> m RootFields
|
||||
combineRootFields rootFields = do
|
||||
let duplicateQueryFields = duplicates $
|
||||
concatMap (Map.keys . _rootQueryFields) rootFields
|
||||
duplicateMutationFields = duplicates $
|
||||
concatMap (Map.keys . _rootMutationFields) rootFields
|
||||
|
||||
pure $ mconcat rootFields
|
||||
-- TODO: The following exception should result in inconsistency
|
||||
when (not $ null duplicateQueryFields) $
|
||||
throw400 Unexpected $ "following query root fields are duplicated: "
|
||||
<> showNames duplicateQueryFields
|
||||
|
||||
getGCtx :: (CacheRM m) => RoleName -> GCtxMap -> m GCtx
|
||||
getGCtx rn ctxMap = do
|
||||
sc <- askSchemaCache
|
||||
return $ fromMaybe (scDefaultRemoteGCtx sc) $ Map.lookup rn ctxMap
|
||||
when (not $ null duplicateMutationFields) $
|
||||
throw400 Unexpected $ "following mutation root fields are duplicated: "
|
||||
<> showNames duplicateMutationFields
|
||||
|
||||
pure $ mconcat rootFields
|
||||
|
||||
getGCtx :: BackendOnlyFieldAccess -> SchemaCache -> RoleName -> GCtx
|
||||
getGCtx backendOnlyFieldAccess sc roleName =
|
||||
case Map.lookup roleName (scGCtxMap sc) of
|
||||
Nothing -> scDefaultRemoteGCtx sc
|
||||
Just (RoleContext defaultGCtx maybeBackendGCtx) ->
|
||||
case backendOnlyFieldAccess of
|
||||
BOFAAllowed ->
|
||||
-- When backend field access is allowed and if there's no 'backend_only'
|
||||
-- permissions defined, we should allow access to non backend only fields
|
||||
fromMaybe defaultGCtx maybeBackendGCtx
|
||||
BOFADisallowed -> defaultGCtx
|
||||
|
||||
-- pretty print GCtx
|
||||
ppGCtx :: GCtx -> String
|
||||
@ -813,12 +874,12 @@ mkGCtx tyAgg (RootFields queryFields mutationFields) insCtxMap =
|
||||
colTys = Set.fromList $ map pgiType $ mapMaybe (^? _RFPGColumn) $
|
||||
Map.elems fldInfos
|
||||
mkMutRoot =
|
||||
mkHsraObjTyInfo (Just "mutation root") (G.NamedType "mutation_root") Set.empty .
|
||||
mkHsraObjTyInfo (Just "mutation root") mutationRootNamedType Set.empty .
|
||||
mapFromL _fiName
|
||||
mutRootM = bool (Just $ mkMutRoot mFlds) Nothing $ null mFlds
|
||||
mkSubRoot =
|
||||
mkHsraObjTyInfo (Just "subscription root")
|
||||
(G.NamedType "subscription_root") Set.empty . mapFromL _fiName
|
||||
subscriptionRootNamedType Set.empty . mapFromL _fiName
|
||||
subRootM = bool (Just $ mkSubRoot qFlds) Nothing $ null qFlds
|
||||
|
||||
qFlds = rootFieldInfos queryFields
|
||||
|
@ -15,6 +15,7 @@ import Hasura.GraphQL.Resolve.Types
|
||||
import Hasura.GraphQL.Validate.Types
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
|
||||
mkAsyncActionSelectionType :: ActionName -> G.NamedType
|
||||
@ -74,7 +75,7 @@ mkQueryActionField actionName actionInfo definitionList =
|
||||
(_adHeaders definition)
|
||||
(_adForwardClientHeaders definition)
|
||||
|
||||
description = mkDescriptionWith (PGDescription <$> (_aiComment actionInfo)) $
|
||||
description = mkDescriptionWith (PGDescription <$> _aiComment actionInfo) $
|
||||
"perform the action: " <>> actionName
|
||||
|
||||
fieldInfo =
|
||||
@ -238,7 +239,7 @@ mkFieldMap annotatedOutputType actionInfo fieldReferences roleName =
|
||||
Nothing -> Nothing
|
||||
|
||||
getFilterAndLimit remoteTableInfo =
|
||||
if roleName == adminRole
|
||||
if roleName == adminRoleName
|
||||
then Just (annBoolExpTrue, Nothing)
|
||||
else do
|
||||
selectPermisisonInfo <-
|
||||
@ -310,12 +311,12 @@ mkMutationActionSchemaOne
|
||||
, (ActionMutationExecutionContext, ObjFldInfo)
|
||||
, FieldMap
|
||||
)
|
||||
mkMutationActionSchemaOne actionInfo kind = do
|
||||
mkMutationActionSchemaOne actionInfo kind =
|
||||
flip Map.map permissions $ \permission ->
|
||||
mkMutationActionFieldsAndTypes actionInfo permission kind
|
||||
where
|
||||
adminPermission = ActionPermissionInfo adminRole
|
||||
permissions = Map.insert adminRole adminPermission $ _aiPermissions actionInfo
|
||||
adminPermission = ActionPermissionInfo adminRoleName
|
||||
permissions = Map.insert adminRoleName adminPermission $ _aiPermissions actionInfo
|
||||
|
||||
mkQueryActionSchemaOne
|
||||
:: ActionInfo
|
||||
@ -327,8 +328,8 @@ mkQueryActionSchemaOne actionInfo =
|
||||
flip Map.map permissions $ \permission ->
|
||||
mkQueryActionFieldsAndTypes actionInfo permission
|
||||
where
|
||||
adminPermission = ActionPermissionInfo adminRole
|
||||
permissions = Map.insert adminRole adminPermission $ _aiPermissions actionInfo
|
||||
adminPermission = ActionPermissionInfo adminRoleName
|
||||
permissions = Map.insert adminRoleName adminPermission $ _aiPermissions actionInfo
|
||||
|
||||
mkActionsSchema
|
||||
:: ActionCache
|
||||
|
@ -12,6 +12,7 @@ import Hasura.GraphQL.Context (defaultTypes)
|
||||
import Hasura.GraphQL.Schema.Common (mkTableTy)
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
|
||||
import qualified Hasura.GraphQL.Validate.Types as VT
|
||||
@ -30,7 +31,7 @@ buildObjectTypeInfo roleName annotatedObjectType =
|
||||
flip map (toList $ _aotRelationships annotatedObjectType) $
|
||||
\(TypeRelationship name ty remoteTableInfo _) ->
|
||||
if isJust (getSelectPermissionInfoM remoteTableInfo roleName) ||
|
||||
roleName == adminRole
|
||||
roleName == adminRoleName
|
||||
then Just (relationshipToFieldInfo name ty $ _tciName $ _tiCoreInfo remoteTableInfo)
|
||||
else Nothing
|
||||
where
|
||||
|
@ -1,5 +1,6 @@
|
||||
module Hasura.GraphQL.Schema.Merge
|
||||
( checkSchemaConflicts
|
||||
( mergeGCtx
|
||||
, checkSchemaConflicts
|
||||
, checkConflictingNode
|
||||
) where
|
||||
|
||||
@ -15,6 +16,33 @@ import Hasura.GraphQL.Validate.Types
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types
|
||||
|
||||
mergeGCtx :: (MonadError QErr m) => GCtx -> GCtx -> m GCtx
|
||||
mergeGCtx lGCtx rGCtx = do
|
||||
checkSchemaConflicts lGCtx rGCtx
|
||||
pure GCtx { _gTypes = mergedTypeMap
|
||||
, _gFields = _gFields lGCtx <> _gFields rGCtx
|
||||
, _gQueryRoot = mergedQueryRoot
|
||||
, _gMutRoot = mergedMutationRoot
|
||||
, _gSubRoot = mergedSubRoot
|
||||
, _gOrdByCtx = _gOrdByCtx lGCtx <> _gOrdByCtx rGCtx
|
||||
, _gQueryCtxMap = _gQueryCtxMap lGCtx <> _gQueryCtxMap rGCtx
|
||||
, _gMutationCtxMap = _gMutationCtxMap lGCtx <> _gMutationCtxMap rGCtx
|
||||
, _gInsCtxMap = _gInsCtxMap lGCtx <> _gInsCtxMap rGCtx
|
||||
}
|
||||
where
|
||||
mergedQueryRoot = _gQueryRoot lGCtx <> _gQueryRoot rGCtx
|
||||
mergedMutationRoot = _gMutRoot lGCtx <> _gMutRoot rGCtx
|
||||
mergedSubRoot = _gSubRoot lGCtx <> _gSubRoot rGCtx
|
||||
mergedTypeMap =
|
||||
let mergedTypes = _gTypes lGCtx <> _gTypes rGCtx
|
||||
modifyQueryRootField = Map.insert queryRootNamedType (TIObj mergedQueryRoot)
|
||||
modifyMaybeRootField tyname maybeObj m = case maybeObj of
|
||||
Nothing -> m
|
||||
Just obj -> Map.insert tyname (TIObj obj) m
|
||||
in modifyMaybeRootField subscriptionRootNamedType mergedSubRoot $
|
||||
modifyMaybeRootField mutationRootNamedType mergedMutationRoot $
|
||||
modifyQueryRootField mergedTypes
|
||||
|
||||
checkSchemaConflicts
|
||||
:: (MonadError QErr m)
|
||||
=> GCtx -> GCtx -> m ()
|
||||
@ -68,8 +96,8 @@ checkSchemaConflicts gCtx remoteCtx = do
|
||||
(TIInpObj t1, TIInpObj t2) -> typeEq t1 t2
|
||||
_ -> False
|
||||
|
||||
hQRName = G.NamedType "query_root"
|
||||
hMRName = G.NamedType "mutation_root"
|
||||
hQRName = queryRootNamedType
|
||||
hMRName = mutationRootNamedType
|
||||
tyMsg ty = "types: [ " <> namesToTxt ty <>
|
||||
" ] have mismatch with current graphql schema. HINT: Types must be same."
|
||||
nodesMsg n = "top-level nodes: [ " <> namesToTxt n <>
|
||||
@ -109,8 +137,8 @@ checkConflictingNode typeMap node = do
|
||||
throw400 RemoteSchemaConflicts msg
|
||||
_ -> return ()
|
||||
where
|
||||
hQRName = G.NamedType "query_root"
|
||||
hMRName = G.NamedType "mutation_root"
|
||||
hQRName = queryRootNamedType
|
||||
hMRName = mutationRootNamedType
|
||||
msg = "node " <> G.unName node <>
|
||||
" already exists in current graphql schema"
|
||||
|
||||
|
@ -15,6 +15,7 @@ import Hasura.RQL.Types
|
||||
import Hasura.Server.Init.Config
|
||||
import Hasura.Server.Utils (RequestId)
|
||||
import Hasura.Server.Version (HasVersion)
|
||||
import Hasura.Session
|
||||
|
||||
import qualified Database.PG.Query as Q
|
||||
import qualified Hasura.GraphQL.Execute as E
|
||||
@ -75,7 +76,7 @@ runGQBatched reqId responseErrorsConfig userInfo reqHdrs reqs =
|
||||
-- It's unclear what we should do if we receive multiple
|
||||
-- responses with distinct headers, so just do the simplest thing
|
||||
-- in this case, and don't forward any.
|
||||
let includeInternal = shouldIncludeInternal (userRole userInfo) responseErrorsConfig
|
||||
let includeInternal = shouldIncludeInternal (_uiRole userInfo) responseErrorsConfig
|
||||
removeHeaders =
|
||||
flip HttpResponse []
|
||||
. encJFromList
|
||||
|
@ -51,6 +51,7 @@ import Hasura.Server.Auth (AuthMode, UserAuth
|
||||
import Hasura.Server.Cors
|
||||
import Hasura.Server.Utils (RequestId, getRequestId)
|
||||
import Hasura.Server.Version (HasVersion)
|
||||
import Hasura.Session
|
||||
|
||||
import qualified Hasura.GraphQL.Execute as E
|
||||
import qualified Hasura.GraphQL.Execute.LiveQuery as LQ
|
||||
@ -160,7 +161,7 @@ $(J.deriveToJSON (J.aesonDrop 5 J.snakeCase) ''WsConnInfo)
|
||||
|
||||
data WSLogInfo
|
||||
= WSLogInfo
|
||||
{ _wsliUserVars :: !(Maybe UserVars)
|
||||
{ _wsliUserVars :: !(Maybe SessionVariables)
|
||||
, _wsliConnectionInfo :: !WsConnInfo
|
||||
, _wsliEvent :: !WSEvent
|
||||
} deriving (Show, Eq)
|
||||
@ -176,11 +177,11 @@ instance L.ToEngineLog WSLog L.Hasura where
|
||||
toEngineLog (WSLog logLevel wsLog) =
|
||||
(logLevel, L.ELTWebsocketLog, J.toJSON wsLog)
|
||||
|
||||
mkWsInfoLog :: Maybe UserVars -> WsConnInfo -> WSEvent -> WSLog
|
||||
mkWsInfoLog :: Maybe SessionVariables -> WsConnInfo -> WSEvent -> WSLog
|
||||
mkWsInfoLog uv ci ev =
|
||||
WSLog L.LevelInfo $ WSLogInfo uv ci ev
|
||||
|
||||
mkWsErrorLog :: Maybe UserVars -> WsConnInfo -> WSEvent -> WSLog
|
||||
mkWsErrorLog :: Maybe SessionVariables -> WsConnInfo -> WSEvent -> WSLog
|
||||
mkWsErrorLog uv ci ev =
|
||||
WSLog L.LevelError $ WSLogInfo uv ci ev
|
||||
|
||||
@ -516,7 +517,7 @@ logWSEvent
|
||||
logWSEvent (L.Logger logger) wsConn wsEv = do
|
||||
userInfoME <- liftIO $ STM.readTVarIO userInfoR
|
||||
let (userVarsM, tokenExpM) = case userInfoME of
|
||||
CSInitialised userInfo tokenM _ -> ( Just $ userVars userInfo
|
||||
CSInitialised userInfo tokenM _ -> ( Just $ _uiSession userInfo
|
||||
, tokenM
|
||||
)
|
||||
_ -> (Nothing, Nothing)
|
||||
|
@ -7,12 +7,13 @@ module Hasura.Incremental.Internal.Dependency where
|
||||
import Hasura.Prelude
|
||||
|
||||
import qualified Data.Dependent.Map as DM
|
||||
import qualified Data.URL.Template as UT
|
||||
import qualified Language.GraphQL.Draft.Syntax as G
|
||||
import qualified Network.URI.Extended as N
|
||||
import qualified Data.URL.Template as UT
|
||||
|
||||
import Control.Applicative
|
||||
import Data.Aeson (Value)
|
||||
import Data.CaseInsensitive (CI)
|
||||
import Data.Functor.Classes (Eq1 (..), Eq2 (..))
|
||||
import Data.GADT.Compare
|
||||
import Data.Int
|
||||
@ -170,6 +171,8 @@ instance (Cacheable k, Cacheable v) => Cacheable (HashMap k v) where
|
||||
unchanged accesses = liftEq2 (unchanged accesses) (unchanged accesses)
|
||||
instance (Cacheable a) => Cacheable (HashSet a) where
|
||||
unchanged = liftEq . unchanged
|
||||
instance (Cacheable a) => Cacheable (CI a) where
|
||||
unchanged _ = (==)
|
||||
|
||||
instance Cacheable ()
|
||||
instance (Cacheable a, Cacheable b) => Cacheable (a, b)
|
||||
|
@ -28,6 +28,7 @@ import Hasura.GraphQL.Context (defaultTypes)
|
||||
import Hasura.GraphQL.Utils
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
|
||||
import qualified Hasura.GraphQL.Validate.Types as VT
|
||||
@ -248,15 +249,15 @@ runCreateActionPermission
|
||||
=> CreateActionPermission -> m EncJSON
|
||||
runCreateActionPermission createActionPermission = do
|
||||
actionInfo <- getActionInfo actionName
|
||||
void $ onJust (Map.lookup role $ _aiPermissions actionInfo) $ const $
|
||||
throw400 AlreadyExists $ "permission for role " <> role
|
||||
void $ onJust (Map.lookup roleName $ _aiPermissions actionInfo) $ const $
|
||||
throw400 AlreadyExists $ "permission for role " <> roleName
|
||||
<<> " is already defined on " <>> actionName
|
||||
persistCreateActionPermission createActionPermission
|
||||
buildSchemaCacheFor $ MOActionPermission actionName role
|
||||
buildSchemaCacheFor $ MOActionPermission actionName roleName
|
||||
pure successMsg
|
||||
where
|
||||
actionName = _capAction createActionPermission
|
||||
role = _capRole createActionPermission
|
||||
roleName = _capRole createActionPermission
|
||||
|
||||
persistCreateActionPermission :: (MonadTx m) => CreateActionPermission -> m ()
|
||||
persistCreateActionPermission CreateActionPermission{..}= do
|
||||
@ -278,15 +279,15 @@ runDropActionPermission
|
||||
=> DropActionPermission -> m EncJSON
|
||||
runDropActionPermission dropActionPermission = do
|
||||
actionInfo <- getActionInfo actionName
|
||||
void $ onNothing (Map.lookup role $ _aiPermissions actionInfo) $
|
||||
void $ onNothing (Map.lookup roleName $ _aiPermissions actionInfo) $
|
||||
throw400 NotExists $
|
||||
"permission for role: " <> role <<> " is not defined on " <>> actionName
|
||||
liftTx $ deleteActionPermissionFromCatalog actionName role
|
||||
buildSchemaCacheFor $ MOActionPermission actionName role
|
||||
"permission for role: " <> roleName <<> " is not defined on " <>> actionName
|
||||
liftTx $ deleteActionPermissionFromCatalog actionName roleName
|
||||
buildSchemaCacheFor $ MOActionPermission actionName roleName
|
||||
return successMsg
|
||||
where
|
||||
actionName = _dapAction dropActionPermission
|
||||
role = _dapRole dropActionPermission
|
||||
roleName = _dapRole dropActionPermission
|
||||
|
||||
deleteActionPermissionFromCatalog :: ActionName -> RoleName -> Q.TxE QErr ()
|
||||
deleteActionPermissionFromCatalog actionName role =
|
||||
|
@ -248,8 +248,8 @@ runDropComputedField (DropComputedField table computedField cascade) = do
|
||||
pure successMsg
|
||||
where
|
||||
purgeComputedFieldDependency = \case
|
||||
SOTableObj qt (TOPerm role permType) | qt == table ->
|
||||
liftTx $ dropPermFromCatalog qt role permType
|
||||
SOTableObj qt (TOPerm roleName permType) | qt == table ->
|
||||
liftTx $ dropPermFromCatalog qt roleName permType
|
||||
d -> throw500 $ "unexpected dependency for computed field "
|
||||
<> computedField <<> "; " <> reportSchemaObj d
|
||||
|
||||
|
@ -335,10 +335,11 @@ replaceMetadataToOrdJSON ( ReplaceMetadata
|
||||
insPermDefToOrdJSON :: Permission.InsPermDef -> AO.Value
|
||||
insPermDefToOrdJSON = permDefToOrdJSON insPermToOrdJSON
|
||||
where
|
||||
insPermToOrdJSON (Permission.InsPerm check set columns) =
|
||||
insPermToOrdJSON (Permission.InsPerm check set columns mBackendOnly) =
|
||||
let columnsPair = ("columns",) . AO.toOrdered <$> columns
|
||||
backendOnlyPair = ("backend_only",) . AO.toOrdered <$> mBackendOnly
|
||||
in AO.object $ [("check", AO.toOrdered check)]
|
||||
<> catMaybes [maybeSetToMaybeOrdPair set, columnsPair]
|
||||
<> catMaybes [maybeSetToMaybeOrdPair set, columnsPair, backendOnlyPair]
|
||||
|
||||
selPermDefToOrdJSON :: Permission.SelPermDef -> AO.Value
|
||||
selPermDefToOrdJSON = permDefToOrdJSON selPermToOrdJSON
|
||||
|
@ -42,6 +42,7 @@ import Hasura.Prelude
|
||||
import Hasura.RQL.DDL.Permission.Internal
|
||||
import Hasura.RQL.DML.Internal hiding (askPermInfo)
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
|
||||
import qualified Database.PG.Query as Q
|
||||
@ -55,12 +56,42 @@ import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.HashSet as HS
|
||||
import qualified Data.Text as T
|
||||
|
||||
{- Note [Backend only permissions]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
As of writing this note, Hasura permission system is meant to be used by the
|
||||
frontend. After introducing "Actions", the webhook handlers now can make GraphQL
|
||||
mutations to the server with some backend logic. These mutations shouldn't be
|
||||
exposed to frontend for any user since they'll bypass the business logic.
|
||||
|
||||
For example:-
|
||||
|
||||
We've a table named "user" and it has a "email" column. We need to validate the
|
||||
email address. So we define an action "create_user" and it expects the same inputs
|
||||
as "insert_user" mutation (generated by Hasura). Now, a role has permission for both
|
||||
actions and insert operation on the table. If the insert permission is not marked
|
||||
as "backend_only: true" then it visible to the frontend client along with "creat_user".
|
||||
|
||||
Backend only permissions adds an additional privilege to Hasura generated operations.
|
||||
Those are accessable only if the request is made with `x-hasura-admin-secret`
|
||||
(if authorization is configured), `x-hasura-use-backend-only-permissions`
|
||||
(value must be set to "true"), `x-hasura-role` to identify the role and other
|
||||
required session variables.
|
||||
|
||||
backend_only `x-hasura-admin-secret` `x-hasura-use-backend-only-permissions` Result
|
||||
------------ --------------------- ------------------------------------- ------
|
||||
FALSE ANY ANY Mutation is always visible
|
||||
TRUE FALSE ANY Mutation is always hidden
|
||||
TRUE TRUE (OR NOT-SET) FALSE Mutation is hidden
|
||||
TRUE TRUE (OR NOT-SET) TRUE Mutation is shown
|
||||
-}
|
||||
|
||||
-- Insert permission
|
||||
data InsPerm
|
||||
= InsPerm
|
||||
{ ipCheck :: !BoolExp
|
||||
, ipSet :: !(Maybe (ColumnValues Value))
|
||||
, ipColumns :: !(Maybe PermColSpec)
|
||||
{ ipCheck :: !BoolExp
|
||||
, ipSet :: !(Maybe (ColumnValues Value))
|
||||
, ipColumns :: !(Maybe PermColSpec)
|
||||
, ipBackendOnly :: !(Maybe Bool) -- see Note [Backend only permissions]
|
||||
} deriving (Show, Eq, Lift, Generic)
|
||||
instance Cacheable InsPerm
|
||||
$(deriveJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''InsPerm)
|
||||
@ -96,7 +127,7 @@ buildInsPermInfo
|
||||
-> FieldInfoMap FieldInfo
|
||||
-> PermDef InsPerm
|
||||
-> m (WithDeps InsPermInfo)
|
||||
buildInsPermInfo tn fieldInfoMap (PermDef _rn (InsPerm checkCond set mCols) _) =
|
||||
buildInsPermInfo tn fieldInfoMap (PermDef _rn (InsPerm checkCond set mCols mBackendOnly) _) =
|
||||
withPathK "permission" $ do
|
||||
(be, beDeps) <- withPathK "check" $ procBoolExp tn fieldInfoMap checkCond
|
||||
(setColsSQL, setHdrs, setColDeps) <- procSetObj tn fieldInfoMap set
|
||||
@ -107,8 +138,9 @@ buildInsPermInfo tn fieldInfoMap (PermDef _rn (InsPerm checkCond set mCols) _) =
|
||||
insColDeps = map (mkColDep DRUntyped tn) insCols
|
||||
deps = mkParentDep tn : beDeps ++ setColDeps ++ insColDeps
|
||||
insColsWithoutPresets = insCols \\ HM.keys setColsSQL
|
||||
return (InsPermInfo (HS.fromList insColsWithoutPresets) be setColsSQL reqHdrs, deps)
|
||||
return (InsPermInfo (HS.fromList insColsWithoutPresets) be setColsSQL backendOnly reqHdrs, deps)
|
||||
where
|
||||
backendOnly = fromMaybe False mBackendOnly
|
||||
allCols = map pgiColumn $ getCols fieldInfoMap
|
||||
insCols = fromMaybe allCols $ convColSpec fieldInfoMap <$> mCols
|
||||
|
||||
@ -202,7 +234,7 @@ data UpdPerm
|
||||
{ ucColumns :: !PermColSpec -- Allowed columns
|
||||
, ucSet :: !(Maybe (ColumnValues Value)) -- Preset columns
|
||||
, ucFilter :: !BoolExp -- Filter expression (applied before update)
|
||||
, ucCheck :: !(Maybe BoolExp)
|
||||
, ucCheck :: !(Maybe BoolExp)
|
||||
-- ^ Check expression, which must be true after update.
|
||||
-- This is optional because we don't want to break the v1 API
|
||||
-- but Nothing should be equivalent to the expression which always
|
||||
@ -224,7 +256,7 @@ buildUpdPermInfo
|
||||
buildUpdPermInfo tn fieldInfoMap (UpdPerm colSpec set fltr check) = do
|
||||
(be, beDeps) <- withPathK "filter" $
|
||||
procBoolExp tn fieldInfoMap fltr
|
||||
|
||||
|
||||
checkExpr <- traverse (withPathK "check" . procBoolExp tn fieldInfoMap) check
|
||||
|
||||
(setColsSQL, setHeaders, setColDeps) <- procSetObj tn fieldInfoMap set
|
||||
@ -333,7 +365,7 @@ setPermCommentTx (SetPermComment (QualifiedObject sn tn) rn pt comment) =
|
||||
|] (comment, sn, tn, rn, permTypeToCode pt) True
|
||||
|
||||
purgePerm :: MonadTx m => QualifiedTable -> RoleName -> PermType -> m ()
|
||||
purgePerm qt rn pt =
|
||||
purgePerm qt rn pt =
|
||||
case pt of
|
||||
PTInsert -> dropPermP2 @InsPerm dp
|
||||
PTSelect -> dropPermP2 @SelPerm dp
|
||||
|
@ -1,7 +1,4 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE TypeFamilyDependencies #-}
|
||||
|
||||
module Hasura.RQL.DDL.Permission.Internal where
|
||||
@ -23,6 +20,7 @@ import Hasura.Prelude
|
||||
import Hasura.RQL.GBoolExp
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Server.Utils
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
import Hasura.SQL.Value
|
||||
|
||||
@ -180,7 +178,7 @@ getDepHeadersFromVal val = case val of
|
||||
where
|
||||
parseOnlyString v = case v of
|
||||
(String t)
|
||||
| isUserVar t -> [T.toLower t]
|
||||
| isSessionVariable t -> [T.toLower t]
|
||||
| isReqUserId t -> [userIdHeader]
|
||||
| otherwise -> []
|
||||
_ -> []
|
||||
@ -197,7 +195,7 @@ valueParser
|
||||
valueParser pgType = \case
|
||||
-- When it is a special variable
|
||||
String t
|
||||
| isUserVar t -> return $ mkTypedSessionVar pgType t
|
||||
| isSessionVariable t -> return $ mkTypedSessionVar pgType $ mkSessionVariable t
|
||||
| isReqUserId t -> return $ mkTypedSessionVar pgType userIdHeader
|
||||
-- Typical value as Aeson's value
|
||||
val -> case pgType of
|
||||
@ -280,7 +278,7 @@ dropPermP1 dp@(DropPerm tn rn) = do
|
||||
askPermInfo tabInfo rn $ getPermAcc2 dp
|
||||
|
||||
dropPermP2 :: forall a m. (MonadTx m, IsPerm a) => DropPerm a -> m ()
|
||||
dropPermP2 dp@(DropPerm tn rn) = do
|
||||
dropPermP2 dp@(DropPerm tn rn) =
|
||||
liftTx $ dropPermFromCatalog tn rn pt
|
||||
where
|
||||
pa = getPermAcc2 dp
|
||||
|
@ -40,6 +40,7 @@ import qualified Language.GraphQL.Draft.Syntax as G
|
||||
import Hasura.Db
|
||||
import Hasura.GraphQL.RemoteServer
|
||||
import Hasura.GraphQL.Schema.CustomTypes
|
||||
import Hasura.GraphQL.Schema.Merge
|
||||
import Hasura.GraphQL.Utils (showNames)
|
||||
import Hasura.RQL.DDL.Action
|
||||
import Hasura.RQL.DDL.ComputedField
|
||||
@ -60,15 +61,15 @@ import Hasura.RQL.Types
|
||||
import Hasura.RQL.Types.Catalog
|
||||
import Hasura.RQL.Types.QueryCollection
|
||||
import Hasura.Server.Version (HasVersion)
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
|
||||
mergeCustomTypes
|
||||
:: MonadError QErr f
|
||||
=> M.HashMap RoleName GS.GCtx -> GS.GCtx -> (NonObjectTypeMap, AnnotatedObjects)
|
||||
=> GS.GCtxMap -> GS.GCtx -> (NonObjectTypeMap, AnnotatedObjects)
|
||||
-> f (GS.GCtxMap, GS.GCtx)
|
||||
mergeCustomTypes gCtxMap remoteSchemaCtx customTypesState = do
|
||||
let adminCustomTypes = buildCustomTypesSchema (fst customTypesState)
|
||||
(snd customTypesState) adminRole
|
||||
let adminCustomTypes = uncurry buildCustomTypesSchema customTypesState adminRoleName
|
||||
let commonTypes = M.intersectionWith (,) existingTypes adminCustomTypes
|
||||
conflictingCustomTypes =
|
||||
map (G.unNamedType . fst) $ M.toList $
|
||||
@ -82,10 +83,10 @@ mergeCustomTypes gCtxMap remoteSchemaCtx customTypesState = do
|
||||
"autogenerated hasura types or from remote schemas: "
|
||||
<> showNames conflictingCustomTypes
|
||||
|
||||
let gCtxMapWithCustomTypes = flip M.mapWithKey gCtxMap $ \roleName gCtx ->
|
||||
let customTypes = buildCustomTypesSchema (fst customTypesState)
|
||||
(snd customTypesState) roleName
|
||||
in addCustomTypes gCtx customTypes
|
||||
let gCtxMapWithCustomTypes = flip M.mapWithKey gCtxMap $ \roleName schemaCtx ->
|
||||
flip fmap schemaCtx $ \gCtx ->
|
||||
let customTypes = uncurry buildCustomTypesSchema customTypesState roleName
|
||||
in addCustomTypes gCtx customTypes
|
||||
|
||||
-- populate the gctx of each role with the custom types
|
||||
return ( gCtxMapWithCustomTypes
|
||||
@ -95,9 +96,9 @@ mergeCustomTypes gCtxMap remoteSchemaCtx customTypesState = do
|
||||
addCustomTypes gCtx customTypes =
|
||||
gCtx { GS._gTypes = GS._gTypes gCtx <> customTypes}
|
||||
existingTypes =
|
||||
case (M.lookup adminRole gCtxMap) of
|
||||
Just gCtx -> GS._gTypes gCtx
|
||||
Nothing -> GS._gTypes remoteSchemaCtx
|
||||
case M.lookup adminRoleName gCtxMap of
|
||||
Just schemaCtx -> GS._gTypes $ GC._rctxDefault schemaCtx
|
||||
Nothing -> GS._gTypes remoteSchemaCtx
|
||||
|
||||
buildRebuildableSchemaCache
|
||||
:: (HasVersion, MonadIO m, MonadUnique m, MonadTx m, HasHttpManager m, HasSQLGenCtx m)
|
||||
|
@ -21,6 +21,7 @@ import Hasura.RQL.DDL.Permission.Internal
|
||||
import Hasura.RQL.DDL.Schema.Cache.Common
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.RQL.Types.Catalog
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
|
||||
buildTablePermissions
|
||||
@ -92,7 +93,7 @@ buildPermission = Inc.cache proc (tableCache, tableName, tableFields, permission
|
||||
(permissions >- noDuplicates mkPermissionMetadataObject)
|
||||
>-> (| traverseA (\permission@(CatalogPermission _ roleName _ pDef _) ->
|
||||
(| withPermission (do
|
||||
bindErrorA -< when (roleName == adminRole) $
|
||||
bindErrorA -< when (roleName == adminRoleName) $
|
||||
throw400 ConstraintViolation "cannot define permission for admin role"
|
||||
perm <- bindErrorA -< decodeValue pDef
|
||||
let permDef = PermDef roleName perm Nothing
|
||||
|
@ -12,14 +12,16 @@ import Control.Arrow ((***))
|
||||
import Control.Lens.Combinators
|
||||
import Control.Lens.Operators
|
||||
import Hasura.Prelude
|
||||
import qualified Hasura.RQL.DDL.EventTrigger as DS
|
||||
import Hasura.RQL.DDL.Permission
|
||||
import Hasura.RQL.DDL.Permission.Internal
|
||||
import Hasura.RQL.DDL.Relationship.Types
|
||||
import Hasura.RQL.DDL.Schema.Catalog
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
|
||||
import qualified Hasura.RQL.DDL.EventTrigger as DS
|
||||
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import qualified Database.PG.Query as Q
|
||||
|
||||
@ -199,16 +201,16 @@ updatePermFlds refQT rn pt rename = do
|
||||
updateInsPermFlds
|
||||
:: (MonadTx m, CacheRM m)
|
||||
=> QualifiedTable -> Rename -> RoleName -> InsPerm -> m ()
|
||||
updateInsPermFlds refQT rename rn (InsPerm chk preset cols) = do
|
||||
updateInsPermFlds refQT rename rn (InsPerm chk preset cols mBackendOnly) = do
|
||||
updatedPerm <- case rename of
|
||||
RTable rt -> do
|
||||
let updChk = updateTableInBoolExp rt chk
|
||||
return $ InsPerm updChk preset cols
|
||||
return $ InsPerm updChk preset cols mBackendOnly
|
||||
RField rf -> do
|
||||
updChk <- updateFieldInBoolExp refQT rf chk
|
||||
let updPresetM = updatePreset refQT rf <$> preset
|
||||
updColsM = updateCols refQT rf <$> cols
|
||||
return $ InsPerm updChk updPresetM updColsM
|
||||
return $ InsPerm updChk updPresetM updColsM mBackendOnly
|
||||
liftTx $ updatePermDefInCatalog PTInsert refQT rn updatedPerm
|
||||
|
||||
updateSelPermFlds
|
||||
|
@ -15,6 +15,7 @@ import Hasura.RQL.DML.Returning
|
||||
import Hasura.RQL.GBoolExp
|
||||
import Hasura.RQL.Instances ()
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
|
||||
import qualified Database.PG.Query as Q
|
||||
@ -95,9 +96,9 @@ convObj prepFn defInsVals setInsVals fieldInfoMap insObj = do
|
||||
preSetCols = HM.keys setInsVals
|
||||
|
||||
throwNotInsErr c = do
|
||||
role <- userRole <$> askUserInfo
|
||||
roleName <- _uiRole <$> askUserInfo
|
||||
throw400 NotSupported $ "column " <> c <<> " is not insertable"
|
||||
<> " for role " <>> role
|
||||
<> " for role " <>> roleName
|
||||
|
||||
validateInpCols :: (MonadError QErr m) => [PGCol] -> [PGCol] -> m ()
|
||||
validateInpCols inpCols updColsPerm = forM_ inpCols $ \inpCol ->
|
||||
|
@ -6,6 +6,7 @@ import qualified Hasura.SQL.DML as S
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.GBoolExp
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Error
|
||||
import Hasura.SQL.Types
|
||||
import Hasura.SQL.Value
|
||||
@ -38,7 +39,7 @@ mkAdminRolePermInfo ti =
|
||||
getComputedFieldInfos fields
|
||||
|
||||
tn = _tciName ti
|
||||
i = InsPermInfo (HS.fromList pgCols) annBoolExpTrue M.empty []
|
||||
i = InsPermInfo (HS.fromList pgCols) annBoolExpTrue M.empty False []
|
||||
s = SelPermInfo (HS.fromList pgCols) (HS.fromList scalarComputedFields) tn annBoolExpTrue
|
||||
Nothing True []
|
||||
u = UpdPermInfo (HS.fromList pgCols) tn annBoolExpTrue Nothing M.empty []
|
||||
@ -56,7 +57,7 @@ askPermInfo' pa tableInfo = do
|
||||
where
|
||||
rpim = _tiRolePermInfoMap tableInfo
|
||||
getRolePermInfo roleName
|
||||
| roleName == adminRole = Just $ mkAdminRolePermInfo (_tiCoreInfo tableInfo)
|
||||
| roleName == adminRoleName = Just $ mkAdminRolePermInfo (_tiCoreInfo tableInfo)
|
||||
| otherwise = M.lookup roleName rpim
|
||||
|
||||
askPermInfo
|
||||
@ -78,9 +79,9 @@ askPermInfo pa tableInfo = do
|
||||
pt = permTypeToCode $ permAccToType pa
|
||||
|
||||
isTabUpdatable :: RoleName -> TableInfo -> Bool
|
||||
isTabUpdatable role ti
|
||||
| role == adminRole = True
|
||||
| otherwise = isJust $ M.lookup role rpim >>= _permUpd
|
||||
isTabUpdatable roleName ti
|
||||
| roleName == adminRoleName = True
|
||||
| otherwise = isJust $ M.lookup roleName rpim >>= _permUpd
|
||||
where
|
||||
rpim = _tiRolePermInfoMap ti
|
||||
|
||||
@ -124,7 +125,7 @@ checkPermOnCol pt allowedCols pgCol = do
|
||||
throw400 PermissionDenied $ permErrMsg roleName
|
||||
where
|
||||
permErrMsg roleName
|
||||
| roleName == adminRole = "no such column exists : " <>> pgCol
|
||||
| roleName == adminRoleName = "no such column exists : " <>> pgCol
|
||||
| otherwise = mconcat
|
||||
[ "role " <>> roleName
|
||||
, " does not have permission to "
|
||||
@ -146,7 +147,7 @@ fetchRelTabInfo refTabName =
|
||||
-- Internal error
|
||||
modifyErrAndSet500 ("foreign " <> ) $ askTabInfo refTabName
|
||||
|
||||
type SessVarBldr m = PGType PGScalarType -> SessVar -> m S.SQLExp
|
||||
type SessVarBldr m = PGType PGScalarType -> SessionVariable -> m S.SQLExp
|
||||
|
||||
fetchRelDet
|
||||
:: (UserInfoM m, QErrM m, CacheRM m)
|
||||
@ -205,11 +206,11 @@ convPartialSQLExp f = \case
|
||||
PSESessVar colTy sessionVariable -> f colTy sessionVariable
|
||||
|
||||
sessVarFromCurrentSetting
|
||||
:: (Applicative f) => PGType PGScalarType -> SessVar -> f S.SQLExp
|
||||
:: (Applicative f) => PGType PGScalarType -> SessionVariable -> f S.SQLExp
|
||||
sessVarFromCurrentSetting pgType sessVar =
|
||||
pure $ sessVarFromCurrentSetting' pgType sessVar
|
||||
|
||||
sessVarFromCurrentSetting' :: PGType PGScalarType -> SessVar -> S.SQLExp
|
||||
sessVarFromCurrentSetting' :: PGType PGScalarType -> SessionVariable -> S.SQLExp
|
||||
sessVarFromCurrentSetting' ty sessVar =
|
||||
flip S.SETyAnn (S.mkTypeAnn ty) $
|
||||
case ty of
|
||||
@ -217,7 +218,7 @@ sessVarFromCurrentSetting' ty sessVar =
|
||||
PGTypeArray _ -> sessVarVal
|
||||
where
|
||||
sessVarVal = S.SEOpApp (S.SQLOp "->>")
|
||||
[currentSession, S.SELit $ T.toLower sessVar]
|
||||
[currentSession, S.SELit $ sessionVariableToText sessVar]
|
||||
|
||||
currentSession :: S.SQLExp
|
||||
currentSession = S.SEUnsafe "current_setting('hasura.user')::json"
|
||||
@ -278,7 +279,7 @@ toJSONableExp strfyNum colTy asText expn
|
||||
-- validate headers
|
||||
validateHeaders :: (UserInfoM m, QErrM m) => [T.Text] -> m ()
|
||||
validateHeaders depHeaders = do
|
||||
headers <- getVarNames . userVars <$> askUserInfo
|
||||
headers <- getSessionVariables . _uiSession <$> askUserInfo
|
||||
forM_ depHeaders $ \hdr ->
|
||||
unless (hdr `elem` map T.toLower headers) $
|
||||
throw400 NotFound $ hdr <<> " header is expected but not found"
|
||||
|
@ -23,6 +23,7 @@ import Hasura.RQL.DML.Returning
|
||||
import Hasura.RQL.GBoolExp
|
||||
import Hasura.RQL.Instances ()
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
|
||||
import qualified Database.PG.Query as Q
|
||||
@ -132,9 +133,9 @@ convOp fieldInfoMap preSetCols updPerm objs conv =
|
||||
allowedCols = upiCols updPerm
|
||||
relWhenPgErr = "relationships can't be updated"
|
||||
throwNotUpdErr c = do
|
||||
role <- userRole <$> askUserInfo
|
||||
roleName <- _uiRole <$> askUserInfo
|
||||
throw400 NotSupported $ "column " <> c <<> " is not updatable"
|
||||
<> " for role " <> role <<> "; its value is predefined in permission"
|
||||
<> " for role " <> roleName <<> "; its value is predefined in permission"
|
||||
|
||||
validateUpdateQueryWith
|
||||
:: (UserInfoM m, QErrM m, CacheRM m)
|
||||
|
@ -40,6 +40,7 @@ module Hasura.RQL.Types
|
||||
|
||||
import Hasura.EncJSON
|
||||
import Hasura.Prelude
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
|
||||
import Hasura.Db as R
|
||||
@ -286,7 +287,7 @@ askFieldInfo m f =
|
||||
]
|
||||
|
||||
askCurRole :: (UserInfoM m) => m RoleName
|
||||
askCurRole = userRole <$> askUserInfo
|
||||
askCurRole = _uiRole <$> askUserInfo
|
||||
|
||||
successMsg :: EncJSON
|
||||
successMsg = "{\"message\":\"success\"}"
|
||||
|
@ -40,7 +40,7 @@ import Hasura.Incremental (Cacheable)
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.DDL.Headers
|
||||
import Hasura.RQL.Types.CustomTypes
|
||||
import Hasura.RQL.Types.Permission
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
import Language.Haskell.TH.Syntax (Lift)
|
||||
|
||||
|
@ -36,24 +36,26 @@ module Hasura.RQL.Types.BoolExp
|
||||
, PreSetColsPartial
|
||||
) where
|
||||
|
||||
import Hasura.Incremental (Cacheable)
|
||||
import Hasura.Incremental (Cacheable)
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types.Column
|
||||
import Hasura.RQL.Types.Common
|
||||
import Hasura.RQL.Types.Permission
|
||||
import qualified Hasura.SQL.DML as S
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
|
||||
import qualified Hasura.SQL.DML as S
|
||||
|
||||
import Control.Lens.Plated
|
||||
import Control.Lens.TH
|
||||
import Data.Aeson
|
||||
import Data.Aeson.Casing
|
||||
import Data.Aeson.Internal
|
||||
import Data.Aeson.TH
|
||||
import qualified Data.Aeson.Types as J
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import Instances.TH.Lift ()
|
||||
import Language.Haskell.TH.Syntax (Lift)
|
||||
import Instances.TH.Lift ()
|
||||
import Language.Haskell.TH.Syntax (Lift)
|
||||
|
||||
import qualified Data.Aeson.Types as J
|
||||
import qualified Data.HashMap.Strict as M
|
||||
|
||||
data GExists a
|
||||
= GExists
|
||||
@ -341,13 +343,13 @@ type PreSetCols = M.HashMap PGCol S.SQLExp
|
||||
|
||||
-- doesn't resolve the session variable
|
||||
data PartialSQLExp
|
||||
= PSESessVar !(PGType PGScalarType) !SessVar
|
||||
= PSESessVar !(PGType PGScalarType) !SessionVariable
|
||||
| PSESQLExp !S.SQLExp
|
||||
deriving (Show, Eq, Generic, Data)
|
||||
instance NFData PartialSQLExp
|
||||
instance Cacheable PartialSQLExp
|
||||
|
||||
mkTypedSessionVar :: PGType PGColumnType -> SessVar -> PartialSQLExp
|
||||
mkTypedSessionVar :: PGType PGColumnType -> SessionVariable -> PartialSQLExp
|
||||
mkTypedSessionVar columnType =
|
||||
PSESessVar (unsafePGColumnToRepresentation <$> columnType)
|
||||
|
||||
|
@ -35,6 +35,7 @@ import Hasura.RQL.Types.Permission
|
||||
import Hasura.RQL.Types.QueryCollection
|
||||
import Hasura.RQL.Types.RemoteSchema
|
||||
import Hasura.RQL.Types.SchemaCache
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
|
||||
newtype CatalogForeignKey
|
||||
|
@ -5,6 +5,7 @@ import qualified Data.HashMap.Strict.Extended as M
|
||||
import Control.Lens hiding ((.=))
|
||||
import Data.Aeson
|
||||
import Hasura.Prelude
|
||||
import Hasura.Session
|
||||
|
||||
import Hasura.RQL.Types.Action
|
||||
import Hasura.RQL.Types.Common
|
||||
@ -62,7 +63,7 @@ moiName objectId = moiTypeName objectId <> " " <> case objectId of
|
||||
in tableObjectName <> " in " <> moiName (MOTable tableName)
|
||||
MOCustomTypes -> "custom_types"
|
||||
MOAction name -> dquoteTxt name
|
||||
MOActionPermission name role -> dquoteTxt role <> " permission in " <> dquoteTxt name
|
||||
MOActionPermission name roleName -> dquoteTxt roleName <> " permission in " <> dquoteTxt name
|
||||
|
||||
data MetadataObject
|
||||
= MetadataObject
|
||||
|
@ -1,118 +1,23 @@
|
||||
module Hasura.RQL.Types.Permission
|
||||
( RoleName(..)
|
||||
, roleNameToTxt
|
||||
|
||||
, SessVar
|
||||
, SessVarVal
|
||||
|
||||
, UserVars
|
||||
, mkUserVars
|
||||
, isUserVar
|
||||
, getVarNames
|
||||
, getVarVal
|
||||
, roleFromVars
|
||||
|
||||
, UserInfo(..)
|
||||
, mkUserInfo
|
||||
, userInfoToList
|
||||
, adminUserInfo
|
||||
, adminRole
|
||||
, isAdmin
|
||||
, PermType(..)
|
||||
, permTypeToCode
|
||||
, PermId(..)
|
||||
) where
|
||||
( PermType(..)
|
||||
, permTypeToCode
|
||||
, PermId(..)
|
||||
) where
|
||||
|
||||
import Hasura.Incremental (Cacheable)
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types.Common (NonEmptyText, adminText, mkNonEmptyText,
|
||||
unNonEmptyText)
|
||||
import Hasura.Server.Utils (adminSecretHeader, deprecatedAccessKeyHeader,
|
||||
isUserVar, userRoleHeader)
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
|
||||
import qualified Database.PG.Query as Q
|
||||
|
||||
import Data.Aeson
|
||||
import Data.Hashable
|
||||
import Instances.TH.Lift ()
|
||||
import Language.Haskell.TH.Syntax (Lift)
|
||||
|
||||
import qualified Data.HashMap.Strict as Map
|
||||
import qualified Data.Text as T
|
||||
import qualified Database.PG.Query as Q
|
||||
import qualified PostgreSQL.Binary.Decoding as PD
|
||||
|
||||
newtype RoleName
|
||||
= RoleName {getRoleTxt :: NonEmptyText}
|
||||
deriving ( Show, Eq, Ord, Hashable, FromJSONKey, ToJSONKey, FromJSON
|
||||
, ToJSON, Q.FromCol, Q.ToPrepArg, Lift, Generic, Arbitrary, NFData, Cacheable )
|
||||
|
||||
instance DQuote RoleName where
|
||||
dquoteTxt = roleNameToTxt
|
||||
|
||||
roleNameToTxt :: RoleName -> Text
|
||||
roleNameToTxt = unNonEmptyText . getRoleTxt
|
||||
|
||||
adminRole :: RoleName
|
||||
adminRole = RoleName adminText
|
||||
|
||||
isAdmin :: RoleName -> Bool
|
||||
isAdmin = (adminRole ==)
|
||||
|
||||
type SessVar = Text
|
||||
type SessVarVal = Text
|
||||
|
||||
newtype UserVars
|
||||
= UserVars { unUserVars :: Map.HashMap SessVar SessVarVal}
|
||||
deriving (Show, Eq, FromJSON, ToJSON, Hashable)
|
||||
|
||||
-- returns Nothing if x-hasura-role is an empty string
|
||||
roleFromVars :: UserVars -> Maybe RoleName
|
||||
roleFromVars uv =
|
||||
getVarVal userRoleHeader uv >>= fmap RoleName . mkNonEmptyText
|
||||
|
||||
getVarVal :: SessVar -> UserVars -> Maybe SessVarVal
|
||||
getVarVal k =
|
||||
Map.lookup (T.toLower 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
|
||||
]
|
||||
|
||||
data UserInfo
|
||||
= UserInfo
|
||||
{ userRole :: !RoleName
|
||||
, userVars :: !UserVars
|
||||
} deriving (Show, Eq, Generic)
|
||||
|
||||
mkUserInfo :: RoleName -> UserVars -> UserInfo
|
||||
mkUserInfo rn (UserVars v) =
|
||||
UserInfo rn $ UserVars $ Map.insert userRoleHeader (roleNameToTxt rn) $
|
||||
foldl (flip Map.delete) v [adminSecretHeader, deprecatedAccessKeyHeader]
|
||||
|
||||
instance Hashable UserInfo
|
||||
|
||||
-- $(J.deriveToJSON (J.aesonDrop 4 J.camelCase){J.omitNothingFields=True}
|
||||
-- ''UserInfo
|
||||
-- )
|
||||
|
||||
userInfoToList :: UserInfo -> [(Text, Text)]
|
||||
userInfoToList userInfo =
|
||||
let vars = Map.toList $ unUserVars . userVars $ userInfo
|
||||
rn = roleNameToTxt . userRole $ userInfo
|
||||
in (userRoleHeader, rn) : vars
|
||||
|
||||
adminUserInfo :: UserInfo
|
||||
adminUserInfo =
|
||||
mkUserInfo adminRole $ mkUserVars []
|
||||
|
||||
data PermType
|
||||
= PTInsert
|
||||
| PTSelect
|
||||
|
@ -7,6 +7,7 @@ module Hasura.RQL.Types.Run
|
||||
) where
|
||||
|
||||
import Hasura.Prelude
|
||||
import Hasura.Session
|
||||
|
||||
import qualified Database.PG.Query as Q
|
||||
import qualified Network.HTTP.Client as HTTP
|
||||
|
@ -12,6 +12,7 @@ import Hasura.RQL.Types.Common
|
||||
import Hasura.RQL.Types.ComputedField
|
||||
import Hasura.RQL.Types.EventTrigger
|
||||
import Hasura.RQL.Types.Permission
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
|
||||
data TableObjId
|
||||
|
@ -77,8 +77,6 @@ module Hasura.RQL.Types.Table
|
||||
|
||||
) where
|
||||
|
||||
-- import qualified Hasura.GraphQL.Context as GC
|
||||
|
||||
import Hasura.GraphQL.Utils (showNames)
|
||||
import Hasura.Incremental (Cacheable)
|
||||
import Hasura.Prelude
|
||||
@ -90,6 +88,7 @@ import Hasura.RQL.Types.Error
|
||||
import Hasura.RQL.Types.EventTrigger
|
||||
import Hasura.RQL.Types.Permission
|
||||
import Hasura.Server.Utils (duplicates)
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
|
||||
import Control.Lens
|
||||
@ -213,6 +212,7 @@ data InsPermInfo
|
||||
{ ipiCols :: !(HS.HashSet PGCol)
|
||||
, ipiCheck :: !AnnBoolExpPartialSQL
|
||||
, ipiSet :: !PreSetColsPartial
|
||||
, ipiBackendOnly :: !Bool
|
||||
, ipiRequiredHeaders :: ![T.Text]
|
||||
} deriving (Show, Eq, Generic)
|
||||
instance NFData InsPermInfo
|
||||
@ -279,12 +279,12 @@ data EventTriggerInfo
|
||||
, etiOpsDef :: !TriggerOpsDef
|
||||
, etiRetryConf :: !RetryConf
|
||||
, etiWebhookInfo :: !WebhookConfInfo
|
||||
-- ^ The HTTP(s) URL which will be called with the event payload on configured operation.
|
||||
-- Must be a POST handler. This URL can be entered manually or can be picked up from an
|
||||
-- environment variable (the environment variable needs to be set before using it for
|
||||
-- this configuration).
|
||||
-- ^ The HTTP(s) URL which will be called with the event payload on configured operation.
|
||||
-- Must be a POST handler. This URL can be entered manually or can be picked up from an
|
||||
-- environment variable (the environment variable needs to be set before using it for
|
||||
-- this configuration).
|
||||
, etiHeaders :: ![EventHeaderInfo]
|
||||
-- ^ Custom headers can be added to an event trigger. Each webhook request will have these
|
||||
-- ^ Custom headers can be added to an event trigger. Each webhook request will have these
|
||||
-- headers added.
|
||||
} deriving (Show, Eq, Generic)
|
||||
instance NFData EventTriggerInfo
|
||||
|
@ -38,6 +38,7 @@ import Hasura.RQL.Types.Run
|
||||
import Hasura.Server.Init (InstanceId (..))
|
||||
import Hasura.Server.Utils
|
||||
import Hasura.Server.Version (HasVersion)
|
||||
import Hasura.Session
|
||||
|
||||
|
||||
data RQLQueryV1
|
||||
|
@ -54,6 +54,7 @@ import Hasura.Server.Middleware (corsMiddleware)
|
||||
import Hasura.Server.Migrate (migrateCatalog)
|
||||
import Hasura.Server.Utils
|
||||
import Hasura.Server.Version
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
|
||||
import qualified Hasura.GraphQL.Execute as E
|
||||
@ -190,8 +191,8 @@ parseBody reqBody =
|
||||
|
||||
onlyAdmin :: (Monad m) => Handler m ()
|
||||
onlyAdmin = do
|
||||
uRole <- asks (userRole . hcUser)
|
||||
when (uRole /= adminRole) $
|
||||
uRole <- asks (_uiRole . hcUser)
|
||||
when (uRole /= adminRoleName) $
|
||||
throw400 AccessDenied "You have to be an admin to access this endpoint"
|
||||
|
||||
buildQCtx :: (MonadIO m) => Handler m QCtx
|
||||
@ -234,7 +235,7 @@ mkSpockAction serverCtx qErrEncoder qErrModifier apiHandler = do
|
||||
return userInfoE
|
||||
|
||||
let handlerState = HandlerCtx serverCtx userInfo headers requestId
|
||||
includeInternal = shouldIncludeInternal (userRole userInfo) $
|
||||
includeInternal = shouldIncludeInternal (_uiRole userInfo) $
|
||||
scResponseInternalErrorsConfig serverCtx
|
||||
|
||||
(serviceTime, (result, q)) <- withElapsedTime $ case apiHandler of
|
||||
|
@ -29,13 +29,13 @@ import qualified Data.Text as T
|
||||
import qualified Network.HTTP.Client as H
|
||||
import qualified Network.HTTP.Types as N
|
||||
|
||||
import Hasura.HTTP
|
||||
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
|
||||
@ -139,9 +139,6 @@ mkJwtCtx JWTConfig{..} httpManager logger = do
|
||||
JFEJwkParseError _ e -> throwError e
|
||||
JFEExpiryParseError _ _ -> return Nothing
|
||||
|
||||
|
||||
|
||||
|
||||
getUserInfo
|
||||
:: (HasVersion, MonadIO m, MonadError QErr m)
|
||||
=> Logger Hasura
|
||||
@ -160,7 +157,7 @@ getUserInfoWithExpTime
|
||||
-> m (UserInfo, Maybe UTCTime)
|
||||
getUserInfoWithExpTime logger manager rawHeaders = \case
|
||||
|
||||
AMNoAuth -> return (userInfoFromHeaders, Nothing)
|
||||
AMNoAuth -> (, Nothing) <$> userInfoFromHeaders UAuthNotSet
|
||||
|
||||
AMAdminSecret adminScrt unAuthRole ->
|
||||
case adminSecretM of
|
||||
@ -183,23 +180,21 @@ getUserInfoWithExpTime logger manager rawHeaders = \case
|
||||
maybe action (withNoExpTime . userInfoWhenAdminSecret ak) adminSecretM
|
||||
|
||||
adminSecretM= foldl1 (<|>) $
|
||||
map (`getVarVal` usrVars) [adminSecretHeader, deprecatedAccessKeyHeader]
|
||||
map (`getSessionVariableValue` sessionVariables) [adminSecretHeader, deprecatedAccessKeyHeader]
|
||||
|
||||
usrVars = mkUserVars $ hdrsToText rawHeaders
|
||||
sessionVariables = mkSessionVariables rawHeaders
|
||||
|
||||
userInfoWhenAdminSecret key reqKey = do
|
||||
when (reqKey /= getAdminSecret key) $ throw401 $
|
||||
"invalid " <> adminSecretHeader <> "/" <> deprecatedAccessKeyHeader
|
||||
return userInfoFromHeaders
|
||||
userInfoFromHeaders UAdminSecretSent
|
||||
|
||||
userInfoWhenNoAdminSecret = \case
|
||||
Nothing -> throw401 $ adminSecretHeader <> "/"
|
||||
<> deprecatedAccessKeyHeader <> " required, but not found"
|
||||
Just role -> return $ mkUserInfo role usrVars
|
||||
Just roleName -> mkUserInfo UAdminSecretNotSent sessionVariables $ Just roleName
|
||||
|
||||
withNoExpTime a = (, Nothing) <$> a
|
||||
|
||||
userInfoFromHeaders =
|
||||
case roleFromVars usrVars of
|
||||
Just rn -> mkUserInfo rn usrVars
|
||||
Nothing -> mkUserInfo adminRole usrVars
|
||||
userInfoFromHeaders uas =
|
||||
mkUserInfo uas sessionVariables $ Just adminRoleName
|
||||
|
@ -30,28 +30,30 @@ import Hasura.HTTP
|
||||
import Hasura.Logging (Hasura, LogLevel (..), Logger (..))
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.RQL.Types.Error (encodeJSONPath)
|
||||
import Hasura.Server.Auth.JWT.Internal (parseHmacKey, parseRsaKey)
|
||||
import Hasura.Server.Auth.JWT.Logging
|
||||
import Hasura.Server.Utils (getRequestHeader, userRoleHeader, executeJSONPath)
|
||||
import Hasura.Server.Utils (executeJSONPath, getRequestHeader,
|
||||
isSessionVariable, userRoleHeader)
|
||||
import Hasura.Server.Version (HasVersion)
|
||||
import Hasura.RQL.Types.Error (encodeJSONPath)
|
||||
import Hasura.Session
|
||||
|
||||
import qualified Control.Concurrent.Extended as C
|
||||
import qualified Crypto.JWT as Jose
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.Casing as J
|
||||
import qualified Data.Aeson.TH as J
|
||||
import qualified Data.Aeson.Internal as J
|
||||
import qualified Data.Aeson.TH as J
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import qualified Data.ByteString.Lazy.Char8 as BLC
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Data.HashMap.Strict as Map
|
||||
import qualified Data.Parser.JSONPath as JSONPath
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as T
|
||||
import qualified Network.HTTP.Client as HTTP
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import qualified Network.Wreq as Wreq
|
||||
import qualified Data.Parser.JSONPath as JSONPath
|
||||
|
||||
newtype RawJWT = RawJWT BL.ByteString
|
||||
|
||||
@ -70,7 +72,7 @@ data JWTConfigClaims
|
||||
|
||||
instance J.ToJSON JWTConfigClaims where
|
||||
toJSON (ClaimNsPath nsPath) = J.String . T.pack $ encodeJSONPath nsPath
|
||||
toJSON (ClaimNs ns) = J.String ns
|
||||
toJSON (ClaimNs ns) = J.String ns
|
||||
|
||||
data JWTConfig
|
||||
= JWTConfig
|
||||
@ -224,8 +226,8 @@ processJwt jwtCtx headers mUnAuthRole =
|
||||
|
||||
withoutAuthZHeader = do
|
||||
unAuthRole <- maybe missingAuthzHeader return mUnAuthRole
|
||||
return $ (, Nothing) $
|
||||
mkUserInfo unAuthRole $ mkUserVars $ hdrsToText headers
|
||||
userInfo <- mkUserInfo UAdminSecretNotSent (mkSessionVariables headers) $ Just unAuthRole
|
||||
pure (userInfo, Nothing)
|
||||
|
||||
missingAuthzHeader =
|
||||
throw400 InvalidHeaders "Missing Authorization header in JWT authentication mode"
|
||||
@ -259,22 +261,22 @@ processAuthZHeader jwtCtx headers authzHeader = do
|
||||
hasuraClaims <- parseObjectFromString claimsFmt hasuraClaimsV
|
||||
|
||||
-- filter only x-hasura claims and convert to lower-case
|
||||
let claimsMap = Map.filterWithKey (\k _ -> isUserVar k)
|
||||
let claimsMap = Map.filterWithKey (\k _ -> isSessionVariable k)
|
||||
$ Map.fromList $ map (first T.toLower)
|
||||
$ Map.toList hasuraClaims
|
||||
|
||||
HasuraClaims allowedRoles defaultRole <- parseHasuraClaims claimsMap
|
||||
let role = getCurrentRole defaultRole
|
||||
let roleName = getCurrentRole defaultRole
|
||||
|
||||
when (role `notElem` allowedRoles) currRoleNotAllowed
|
||||
when (roleName `notElem` allowedRoles) currRoleNotAllowed
|
||||
let finalClaims =
|
||||
Map.delete defaultRoleClaim . Map.delete allowedRolesClaim $ claimsMap
|
||||
|
||||
-- transform the map of text:aeson-value -> text:text
|
||||
metadata <- decodeJSON $ J.Object finalClaims
|
||||
|
||||
return $ (, expTimeM) $ mkUserInfo role $ mkUserVars $ Map.toList metadata
|
||||
|
||||
userInfo <- mkUserInfo UAdminSecretNotSent
|
||||
(mkSessionVariablesText $ Map.toList metadata) $ Just roleName
|
||||
pure (userInfo, expTimeM)
|
||||
where
|
||||
parseAuthzHeader = do
|
||||
let tokenParts = BLC.words authzHeader
|
||||
@ -302,7 +304,7 @@ processAuthZHeader jwtCtx headers authzHeader = do
|
||||
claimsLocation =
|
||||
case jcxClaimNs jwtCtx of
|
||||
ClaimNsPath path -> T.pack $ "claims_namespace_path " <> encodeJSONPath path
|
||||
ClaimNs ns -> "claims_namespace " <> ns
|
||||
ClaimNs ns -> "claims_namespace " <> ns
|
||||
|
||||
claimsErr = throw400 JWTInvalidClaims
|
||||
|
||||
@ -312,7 +314,7 @@ processAuthZHeader jwtCtx headers authzHeader = do
|
||||
-- see if there is a x-hasura-role header, or else pick the default role
|
||||
getCurrentRole defaultRole =
|
||||
let mUserRole = getRequestHeader userRoleHeader headers
|
||||
in maybe defaultRole RoleName $ mUserRole >>= mkNonEmptyText . bsToTxt
|
||||
in fromMaybe defaultRole $ mUserRole >>= mkRoleName . bsToTxt
|
||||
|
||||
decodeJSON val = case J.fromJSON val of
|
||||
J.Error e -> throw400 JWTInvalidClaims ("x-hasura-* claims: " <> T.pack e)
|
||||
@ -332,9 +334,10 @@ processAuthZHeader jwtCtx headers authzHeader = do
|
||||
throw400 AccessDenied "Your current role is not in allowed roles"
|
||||
claimsNotFound = do
|
||||
let claimsNsError = case jcxClaimNs jwtCtx of
|
||||
ClaimNsPath path -> T.pack $ "claims not found at claims_namespace_path: '" <> (encodeJSONPath path) <> "'"
|
||||
ClaimNsPath path -> T.pack $ "claims not found at claims_namespace_path: '"
|
||||
<> encodeJSONPath path <> "'"
|
||||
ClaimNs ns -> "claims key: '" <> ns <> "' not found"
|
||||
throw400 JWTInvalidClaims $ claimsNsError
|
||||
throw400 JWTInvalidClaims claimsNsError
|
||||
|
||||
|
||||
-- parse x-hasura-allowed-roles, x-hasura-default-role from JWT claims
|
||||
@ -406,7 +409,7 @@ instance J.ToJSON JWTConfig where
|
||||
|
||||
claimsNsFields = case claimNs of
|
||||
ClaimNsPath nsPath ->
|
||||
["claims_namespace_path" J..= (encodeJSONPath nsPath)]
|
||||
["claims_namespace_path" J..= encodeJSONPath nsPath]
|
||||
ClaimNs ns -> ["claims_namespace" J..= J.String ns]
|
||||
|
||||
sharedFields = [ "claims_format" J..= claimsFmt
|
||||
|
@ -28,6 +28,7 @@ import Hasura.Prelude
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Server.Logging
|
||||
import Hasura.Server.Utils
|
||||
import Hasura.Session
|
||||
|
||||
|
||||
data AuthHookType
|
||||
@ -87,7 +88,7 @@ userInfoFromAuthHook logger manager hook reqHeaders = do
|
||||
unLogger logger $
|
||||
WebHookLog LevelError Nothing (ahUrl hook) (hookMethod hook)
|
||||
(Just $ HttpException err) Nothing Nothing
|
||||
throw500 $ "webhook authentication request failed"
|
||||
throw500 "webhook authentication request failed"
|
||||
|
||||
|
||||
mkUserInfoFromResp
|
||||
@ -115,15 +116,12 @@ mkUserInfoFromResp (Logger logger) url method statusCode respBody
|
||||
throw500 "Invalid response from authorization hook"
|
||||
where
|
||||
getUserInfoFromHdrs rawHeaders = do
|
||||
let usrVars = mkUserVars $ Map.toList rawHeaders
|
||||
case roleFromVars usrVars of
|
||||
Nothing -> do
|
||||
logError
|
||||
throw500 "missing x-hasura-role key in webhook response"
|
||||
Just rn -> do
|
||||
logWebHookResp LevelInfo Nothing Nothing
|
||||
expiration <- runMaybeT $ timeFromCacheControl rawHeaders <|> timeFromExpires rawHeaders
|
||||
return (mkUserInfo rn usrVars, expiration)
|
||||
userInfo <- mkUserInfo UAdminSecretNotSent
|
||||
(mkSessionVariablesText $ Map.toList rawHeaders)
|
||||
Nothing
|
||||
logWebHookResp LevelInfo Nothing Nothing
|
||||
expiration <- runMaybeT $ timeFromCacheControl rawHeaders <|> timeFromExpires rawHeaders
|
||||
pure (userInfo, expiration)
|
||||
|
||||
logWebHookResp :: MonadIO m => LogLevel -> Maybe BL.ByteString -> Maybe T.Text -> m ()
|
||||
logWebHookResp logLevel mResp message =
|
||||
|
@ -59,13 +59,13 @@ import qualified Hasura.Logging as L
|
||||
|
||||
import Hasura.Db
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types (QErr, RoleName (..), SchemaCache (..),
|
||||
mkNonEmptyText)
|
||||
import Hasura.RQL.Types (QErr, SchemaCache (..))
|
||||
import Hasura.Server.Auth
|
||||
import Hasura.Server.Cors
|
||||
import Hasura.Server.Init.Config
|
||||
import Hasura.Server.Logging
|
||||
import Hasura.Server.Utils
|
||||
import Hasura.Session
|
||||
import Network.URI (parseURI)
|
||||
|
||||
newtype DbUid
|
||||
@ -622,7 +622,7 @@ parseConnParams =
|
||||
help (snd pgTimeoutEnv)
|
||||
)
|
||||
allowPrepare = optional $
|
||||
option (eitherReader parseStrAsBool)
|
||||
option (eitherReader parseStringAsBool)
|
||||
( long "use-prepared-statements" <>
|
||||
metavar "<true|false>" <>
|
||||
help (snd pgUsePrepareEnv)
|
||||
@ -689,13 +689,13 @@ jwtSecretHelp = "The JSON containing type and the JWK used for verifying. e.g: "
|
||||
<> "`{\"type\": \"RS256\", \"key\": \"<your-PEM-RSA-public-key>\", \"claims_namespace\": \"<optional-custom-claims-key-name>\"}`"
|
||||
|
||||
parseUnAuthRole :: Parser (Maybe RoleName)
|
||||
parseUnAuthRole = fmap mkRoleName $ optional $
|
||||
parseUnAuthRole = fmap mkRoleName' $ optional $
|
||||
strOption ( long "unauthorized-role" <>
|
||||
metavar "<ROLE>" <>
|
||||
help (snd unAuthRoleEnv)
|
||||
)
|
||||
where
|
||||
mkRoleName mText = mText >>= (fmap RoleName . mkNonEmptyText)
|
||||
mkRoleName' mText = mText >>= mkRoleName
|
||||
|
||||
parseCorsConfig :: Parser (Maybe CorsConfig)
|
||||
parseCorsConfig = mapCC <$> disableCors <*> corsDomain
|
||||
@ -730,7 +730,7 @@ parseConsoleAssetsDir = optional $
|
||||
|
||||
parseEnableTelemetry :: Parser (Maybe Bool)
|
||||
parseEnableTelemetry = optional $
|
||||
option (eitherReader parseStrAsBool)
|
||||
option (eitherReader parseStringAsBool)
|
||||
( long "enable-telemetry" <>
|
||||
help (snd enableTelemetryEnv)
|
||||
)
|
||||
|
@ -18,9 +18,9 @@ import qualified Hasura.GraphQL.Execute.LiveQuery as LQ
|
||||
import qualified Hasura.Logging as L
|
||||
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types (RoleName (..), isAdmin, mkNonEmptyText)
|
||||
import Hasura.Server.Auth
|
||||
import Hasura.Server.Cors
|
||||
import Hasura.Session
|
||||
|
||||
data RawConnParams
|
||||
= RawConnParams
|
||||
@ -222,9 +222,9 @@ instance FromEnv AdminSecret where
|
||||
fromEnv = Right . AdminSecret . T.pack
|
||||
|
||||
instance FromEnv RoleName where
|
||||
fromEnv string = case mkNonEmptyText (T.pack string) of
|
||||
Nothing -> Left "empty string not allowed"
|
||||
Just neText -> Right $ RoleName neText
|
||||
fromEnv string = case mkRoleName (T.pack string) of
|
||||
Nothing -> Left "empty string not allowed"
|
||||
Just roleName -> Right roleName
|
||||
|
||||
instance FromEnv Bool where
|
||||
fromEnv = parseStrAsBool
|
||||
|
@ -41,6 +41,7 @@ import Hasura.Prelude
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Server.Compression
|
||||
import Hasura.Server.Utils
|
||||
import Hasura.Session
|
||||
|
||||
data StartupLog
|
||||
= StartupLog
|
||||
@ -191,7 +192,7 @@ instance ToJSON HttpInfoLog where
|
||||
data OperationLog
|
||||
= OperationLog
|
||||
{ olRequestId :: !RequestId
|
||||
, olUserVars :: !(Maybe UserVars)
|
||||
, olUserVars :: !(Maybe SessionVariables)
|
||||
, olResponseSize :: !(Maybe Int64)
|
||||
, olRequestReadTime :: !(Maybe Seconds)
|
||||
-- ^ Request IO wait time, i.e. time spent reading the full request from the socket.
|
||||
@ -235,7 +236,7 @@ mkHttpAccessLogContext userInfoM reqId req res mTiming compressTypeM headers =
|
||||
}
|
||||
op = OperationLog
|
||||
{ olRequestId = reqId
|
||||
, olUserVars = userVars <$> userInfoM
|
||||
, olUserVars = _uiSession <$> userInfoM
|
||||
, olResponseSize = respSize
|
||||
, olRequestReadTime = Seconds . fst <$> mTiming
|
||||
, olQueryExecutionTime = Seconds . snd <$> mTiming
|
||||
@ -271,7 +272,7 @@ mkHttpErrorLogContext userInfoM reqId req err query mTiming compressTypeM header
|
||||
}
|
||||
op = OperationLog
|
||||
{ olRequestId = reqId
|
||||
, olUserVars = userVars <$> userInfoM
|
||||
, olUserVars = _uiSession <$> userInfoM
|
||||
, olResponseSize = Just $ BL.length $ encode err
|
||||
, olRequestReadTime = Seconds . fst <$> mTiming
|
||||
, olQueryExecutionTime = Seconds . snd <$> mTiming
|
||||
|
@ -3,6 +3,7 @@ module Hasura.Server.SchemaUpdate
|
||||
where
|
||||
|
||||
import Hasura.Prelude
|
||||
import Hasura.Session
|
||||
|
||||
import Hasura.Logging
|
||||
import Hasura.RQL.DDL.Schema (runCacheRWT)
|
||||
|
@ -10,10 +10,10 @@ module Hasura.Server.Telemetry
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Exception (try)
|
||||
import Control.Exception (try)
|
||||
import Control.Lens
|
||||
import Data.List
|
||||
import Data.Text.Conversions (UTF8 (..), decodeText)
|
||||
import Data.Text.Conversions (UTF8 (..), decodeText)
|
||||
|
||||
import Hasura.HTTP
|
||||
import Hasura.Logging
|
||||
@ -22,19 +22,20 @@ import Hasura.RQL.Types
|
||||
import Hasura.Server.Init
|
||||
import Hasura.Server.Telemetry.Counters
|
||||
import Hasura.Server.Version
|
||||
import Hasura.Session
|
||||
|
||||
import qualified CI
|
||||
import qualified Control.Concurrent.Extended as C
|
||||
import qualified Data.Aeson as A
|
||||
import qualified Data.Aeson.Casing as A
|
||||
import qualified Data.Aeson.TH as A
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import qualified Data.HashMap.Strict as Map
|
||||
import qualified Data.Text as T
|
||||
import qualified Network.HTTP.Client as HTTP
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import qualified Network.Wreq as Wreq
|
||||
import qualified Language.GraphQL.Draft.Syntax as G
|
||||
import qualified Control.Concurrent.Extended as C
|
||||
import qualified Data.Aeson as A
|
||||
import qualified Data.Aeson.Casing as A
|
||||
import qualified Data.Aeson.TH as A
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import qualified Data.HashMap.Strict as Map
|
||||
import qualified Data.Text as T
|
||||
import qualified Language.GraphQL.Draft.Syntax as G
|
||||
import qualified Network.HTTP.Client as HTTP
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import qualified Network.Wreq as Wreq
|
||||
|
||||
data RelationshipMetric
|
||||
= RelationshipMetric
|
||||
@ -111,7 +112,7 @@ mkPayload dbId instanceId version metrics = do
|
||||
-- hours. The send time depends on when the server was started and will
|
||||
-- naturally drift.
|
||||
runTelemetry
|
||||
:: (HasVersion)
|
||||
:: HasVersion
|
||||
=> Logger Hasura
|
||||
-> HTTP.Manager
|
||||
-> IO SchemaCache
|
||||
@ -175,7 +176,7 @@ computeMetrics sc _mtServiceTimings _mtPgVersion =
|
||||
countUserTables predicate = length . filter predicate $ Map.elems userTables
|
||||
|
||||
calcPerms :: (RolePermInfo -> Maybe a) -> [RolePermInfo] -> Int
|
||||
calcPerms fn perms = length $ catMaybes $ map fn perms
|
||||
calcPerms fn perms = length $ mapMaybe fn perms
|
||||
|
||||
permsOfTbl :: TableInfo -> [(RoleName, RolePermInfo)]
|
||||
permsOfTbl = Map.toList . _tiRolePermInfoMap
|
||||
|
@ -63,11 +63,27 @@ userIdHeader = "x-hasura-user-id"
|
||||
requestIdHeader :: IsString a => a
|
||||
requestIdHeader = "x-request-id"
|
||||
|
||||
useBackendOnlyPermissionsHeader :: IsString a => a
|
||||
useBackendOnlyPermissionsHeader = "x-hasura-use-backend-only-permissions"
|
||||
|
||||
getRequestHeader :: HTTP.HeaderName -> [HTTP.Header] -> Maybe B.ByteString
|
||||
getRequestHeader hdrName hdrs = snd <$> mHeader
|
||||
where
|
||||
mHeader = find (\h -> fst h == hdrName) hdrs
|
||||
|
||||
parseStringAsBool :: String -> Either String Bool
|
||||
parseStringAsBool t
|
||||
| map toLower t `elem` truthVals = Right True
|
||||
| map toLower t `elem` falseVals = Right False
|
||||
| otherwise = Left errMsg
|
||||
where
|
||||
truthVals = ["true", "t", "yes", "y"]
|
||||
falseVals = ["false", "f", "no", "n"]
|
||||
|
||||
errMsg = " Not a valid boolean text. " ++ "True values are "
|
||||
++ show truthVals ++ " and False values are " ++ show falseVals
|
||||
++ ". All values are case insensitive"
|
||||
|
||||
getRequestId :: (MonadIO m) => [HTTP.Header] -> m RequestId
|
||||
getRequestId headers =
|
||||
-- generate a request id for every request if the client has not sent it
|
||||
@ -154,14 +170,14 @@ commonResponseHeadersIgnored =
|
||||
, "Content-Type", "Content-Length"
|
||||
]
|
||||
|
||||
isUserVar :: Text -> Bool
|
||||
isUserVar = T.isPrefixOf "x-hasura-" . T.toLower
|
||||
isSessionVariable :: Text -> Bool
|
||||
isSessionVariable = T.isPrefixOf "x-hasura-" . T.toLower
|
||||
|
||||
mkClientHeadersForward :: [HTTP.Header] -> [HTTP.Header]
|
||||
mkClientHeadersForward reqHeaders =
|
||||
xForwardedHeaders <> (filterUserVars . filterRequestHeaders) reqHeaders
|
||||
xForwardedHeaders <> (filterSessionVariables . filterRequestHeaders) reqHeaders
|
||||
where
|
||||
filterUserVars = filter (\(k, _) -> not $ isUserVar $ bsToTxt $ CI.original k)
|
||||
filterSessionVariables = filter (\(k, _) -> not $ isSessionVariable $ bsToTxt $ CI.original k)
|
||||
xForwardedHeaders = flip mapMaybe reqHeaders $ \(hdrName, hdrValue) ->
|
||||
case hdrName of
|
||||
"Host" -> Just ("X-Forwarded-Host", hdrValue)
|
||||
|
176
server/src-lib/Hasura/Session.hs
Normal file
176
server/src-lib/Hasura/Session.hs
Normal file
@ -0,0 +1,176 @@
|
||||
module Hasura.Session
|
||||
( RoleName
|
||||
, mkRoleName
|
||||
, adminRoleName
|
||||
, isAdmin
|
||||
, roleNameToTxt
|
||||
, SessionVariable
|
||||
, mkSessionVariable
|
||||
, SessionVariables
|
||||
, sessionVariableToText
|
||||
, mkSessionVariablesText
|
||||
, mkSessionVariables
|
||||
, sessionVariablesToHeaders
|
||||
, getSessionVariableValue
|
||||
, getSessionVariables
|
||||
, UserAdminSecret(..)
|
||||
, UserInfo
|
||||
, _uiRole
|
||||
, _uiSession
|
||||
, _uiBackendOnlyFieldAccess
|
||||
, mkUserInfo
|
||||
, adminUserInfo
|
||||
, BackendOnlyFieldAccess(..)
|
||||
) where
|
||||
|
||||
import Hasura.Incremental (Cacheable)
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types.Common (NonEmptyText, adminText, mkNonEmptyText,
|
||||
unNonEmptyText)
|
||||
import Hasura.RQL.Types.Error
|
||||
import Hasura.Server.Utils
|
||||
import Hasura.SQL.Types
|
||||
|
||||
import Data.Aeson
|
||||
import Instances.TH.Lift ()
|
||||
import Language.Haskell.TH.Syntax (Lift)
|
||||
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Data.HashMap.Strict as Map
|
||||
import qualified Data.Text as T
|
||||
import qualified Database.PG.Query as Q
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
|
||||
newtype RoleName
|
||||
= RoleName {getRoleTxt :: NonEmptyText}
|
||||
deriving ( Show, Eq, Ord, Hashable, FromJSONKey, ToJSONKey, FromJSON
|
||||
, ToJSON, Q.FromCol, Q.ToPrepArg, Lift, Generic, Arbitrary, NFData, Cacheable )
|
||||
|
||||
instance DQuote RoleName where
|
||||
dquoteTxt = roleNameToTxt
|
||||
|
||||
roleNameToTxt :: RoleName -> Text
|
||||
roleNameToTxt = unNonEmptyText . getRoleTxt
|
||||
|
||||
mkRoleName :: Text -> Maybe RoleName
|
||||
mkRoleName = fmap RoleName . mkNonEmptyText
|
||||
|
||||
adminRoleName :: RoleName
|
||||
adminRoleName = RoleName adminText
|
||||
|
||||
isAdmin :: RoleName -> Bool
|
||||
isAdmin = (adminRoleName ==)
|
||||
|
||||
newtype SessionVariable = SessionVariable {unSessionVariable :: CI.CI Text}
|
||||
deriving (Show, Eq, Hashable, IsString, Cacheable, Data, NFData)
|
||||
|
||||
instance ToJSON SessionVariable where
|
||||
toJSON = toJSON . CI.original . unSessionVariable
|
||||
|
||||
sessionVariableToText :: SessionVariable -> Text
|
||||
sessionVariableToText = T.toLower . CI.original . unSessionVariable
|
||||
|
||||
mkSessionVariable :: Text -> SessionVariable
|
||||
mkSessionVariable = SessionVariable . CI.mk
|
||||
|
||||
type SessionVariableValue = Text
|
||||
|
||||
newtype SessionVariables =
|
||||
SessionVariables { unSessionVariables :: Map.HashMap SessionVariable SessionVariableValue}
|
||||
deriving (Show, Eq, Hashable, Semigroup, Monoid)
|
||||
|
||||
instance ToJSON SessionVariables where
|
||||
toJSON (SessionVariables varMap) =
|
||||
toJSON $ Map.fromList $ map (first sessionVariableToText) $ Map.toList varMap
|
||||
|
||||
instance FromJSON SessionVariables where
|
||||
parseJSON v = mkSessionVariablesText . Map.toList <$> parseJSON v
|
||||
|
||||
mkSessionVariablesText :: [(Text, Text)] -> SessionVariables
|
||||
mkSessionVariablesText =
|
||||
SessionVariables . Map.fromList . map (first mkSessionVariable)
|
||||
|
||||
mkSessionVariables :: [HTTP.Header] -> SessionVariables
|
||||
mkSessionVariables =
|
||||
SessionVariables
|
||||
. Map.fromList
|
||||
. map (first SessionVariable)
|
||||
. filter (isSessionVariable . CI.original . fst) -- Only x-hasura-* headers
|
||||
. map (CI.map bsToTxt *** bsToTxt)
|
||||
|
||||
sessionVariablesToHeaders :: SessionVariables -> [HTTP.Header]
|
||||
sessionVariablesToHeaders =
|
||||
map ((CI.map txtToBs . unSessionVariable) *** txtToBs)
|
||||
. Map.toList
|
||||
. unSessionVariables
|
||||
|
||||
getSessionVariables :: SessionVariables -> [Text]
|
||||
getSessionVariables = map sessionVariableToText . Map.keys . unSessionVariables
|
||||
|
||||
getSessionVariableValue :: SessionVariable -> SessionVariables -> Maybe SessionVariableValue
|
||||
getSessionVariableValue k = Map.lookup k . unSessionVariables
|
||||
|
||||
-- | Represent the admin secret state; whether the secret is sent
|
||||
-- in the request or if actually authorization is not configured.
|
||||
data UserAdminSecret
|
||||
= UAdminSecretSent
|
||||
| UAdminSecretNotSent
|
||||
| UAuthNotSet
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | Represents the 'X-Hasura-Use-Backend-Only-Permissions' session variable
|
||||
-- and request made with 'X-Hasura-Admin-Secret' if any auth configured.
|
||||
-- For more details see Note [Backend only permissions]
|
||||
data BackendOnlyFieldAccess
|
||||
= BOFAAllowed
|
||||
| BOFADisallowed
|
||||
deriving (Show, Eq, Generic)
|
||||
instance Hashable BackendOnlyFieldAccess
|
||||
|
||||
data UserInfo
|
||||
= UserInfo
|
||||
{ _uiRole :: !RoleName
|
||||
, _uiSession :: !SessionVariables
|
||||
, _uiBackendOnlyFieldAccess :: !BackendOnlyFieldAccess
|
||||
} deriving (Show, Eq, Generic)
|
||||
instance Hashable UserInfo
|
||||
|
||||
-- | Build user info from @'SessionVariables'
|
||||
mkUserInfo
|
||||
:: (MonadError QErr m)
|
||||
=> UserAdminSecret
|
||||
-> SessionVariables
|
||||
-> Maybe RoleName -- ^ Default role if x-hasura-role session variable not found
|
||||
-> m UserInfo
|
||||
mkUserInfo userAdminSecret sess@(SessionVariables sessVars) defaultRole = do
|
||||
roleName <- onNothing (maybeRoleFromSession <|> defaultRole) $
|
||||
throw400 InvalidParams $ userRoleHeader <> " not found in session variables"
|
||||
-- see Note [Backend only permissions] to know more about the following logic.
|
||||
backendOnlyFieldAccess <- case userAdminSecret of
|
||||
UAdminSecretNotSent -> pure BOFADisallowed
|
||||
UAdminSecretSent -> lookForBackendOnlyPermissionsConfig
|
||||
UAuthNotSet -> lookForBackendOnlyPermissionsConfig
|
||||
let modifiedSession = SessionVariables $ modifySessionVariables roleName sessVars
|
||||
pure $ UserInfo roleName modifiedSession backendOnlyFieldAccess
|
||||
where
|
||||
-- Add x-hasura-role header and remove admin secret headers
|
||||
modifySessionVariables roleName
|
||||
= Map.insert userRoleHeader (roleNameToTxt roleName)
|
||||
. Map.delete adminSecretHeader
|
||||
. Map.delete deprecatedAccessKeyHeader
|
||||
|
||||
-- returns Nothing if x-hasura-role is an empty string
|
||||
maybeRoleFromSession =
|
||||
getSessionVariableValue userRoleHeader sess >>= mkRoleName
|
||||
|
||||
lookForBackendOnlyPermissionsConfig =
|
||||
case getSessionVariableValue useBackendOnlyPermissionsHeader sess of
|
||||
Nothing -> pure BOFADisallowed
|
||||
Just varVal ->
|
||||
case parseStringAsBool (T.unpack varVal) of
|
||||
Left err -> throw400 BadRequest $
|
||||
useBackendOnlyPermissionsHeader <> ": " <> T.pack err
|
||||
Right privilege -> pure $ if privilege then BOFAAllowed else BOFADisallowed
|
||||
|
||||
adminUserInfo :: UserInfo
|
||||
adminUserInfo = UserInfo adminRoleName mempty BOFADisallowed
|
@ -18,12 +18,13 @@ import qualified Network.HTTP.Client.TLS as HTTP
|
||||
import qualified Test.Hspec.Runner as Hspec
|
||||
|
||||
import Hasura.Db (PGExecCtx (..))
|
||||
import Hasura.RQL.Types (SQLGenCtx (..), adminUserInfo)
|
||||
import Hasura.RQL.Types (SQLGenCtx (..))
|
||||
import Hasura.RQL.Types.Run
|
||||
import Hasura.Server.Init (RawConnInfo, mkConnInfo, mkRawConnInfo,
|
||||
parseRawConnInfo, runWithEnv)
|
||||
import Hasura.Server.Migrate
|
||||
import Hasura.Server.Version
|
||||
import Hasura.Session (adminUserInfo)
|
||||
|
||||
import qualified Data.Parser.CacheControlSpec as CacheControlParser
|
||||
import qualified Data.Parser.JSONPathSpec as JsonPath
|
||||
|
@ -0,0 +1,22 @@
|
||||
description: As backend user without header
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
headers:
|
||||
X-Hasura-Role: backend_user
|
||||
response:
|
||||
errors:
|
||||
- extensions:
|
||||
path: $
|
||||
code: validation-failed
|
||||
message: no mutations exist
|
||||
query:
|
||||
query: |
|
||||
mutation {
|
||||
insert_user(objects: [
|
||||
{
|
||||
name: "FooBar"
|
||||
}
|
||||
]){
|
||||
affected_rows
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
description: As backend user with header and invalid boolean value
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
headers:
|
||||
X-Hasura-Role: backend_user
|
||||
X-Hasura-Use-Backend-Only-Permissions: random
|
||||
response:
|
||||
errors:
|
||||
- extensions:
|
||||
path: $
|
||||
code: bad-request
|
||||
message: 'x-hasura-use-backend-only-permissions: Not a valid boolean text. True values are
|
||||
["true","t","yes","y"] and False values are ["false","f","no","n"]. All values
|
||||
are case insensitive'
|
||||
query:
|
||||
query: |
|
||||
mutation {
|
||||
insert_user(objects: [
|
||||
{
|
||||
name: "FooBar"
|
||||
}
|
||||
]){
|
||||
affected_rows
|
||||
returning {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
description: As backend user with header
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
headers:
|
||||
X-Hasura-Role: backend_user
|
||||
X-Hasura-Use-Backend-Only-Permissions: 'true'
|
||||
response:
|
||||
data:
|
||||
insert_user:
|
||||
affected_rows: 1
|
||||
returning:
|
||||
- name: FooBar
|
||||
query:
|
||||
query: |
|
||||
mutation {
|
||||
insert_user(objects: [
|
||||
{
|
||||
name: "FooBar"
|
||||
}
|
||||
]){
|
||||
affected_rows
|
||||
returning {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
description: As backend user with header. This test is run only if any authorization is configured and without admin secret header.
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
headers:
|
||||
X-Hasura-Role: backend_user
|
||||
X-Hasura-Use-Backend-Only-Permissions: 'true'
|
||||
response:
|
||||
errors:
|
||||
- extensions:
|
||||
path: $
|
||||
code: validation-failed
|
||||
message: no mutations exist
|
||||
query:
|
||||
query: |
|
||||
mutation {
|
||||
insert_user(objects: [
|
||||
{
|
||||
name: "FooBar"
|
||||
}
|
||||
]){
|
||||
affected_rows
|
||||
returning {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
@ -463,3 +463,33 @@ args:
|
||||
_where:
|
||||
id: X-Hasura-User-Id
|
||||
is_admin: true
|
||||
|
||||
- type: create_insert_permission
|
||||
args:
|
||||
table: user
|
||||
role: backend_user
|
||||
permission:
|
||||
check: {}
|
||||
columns: '*'
|
||||
backend_only: true
|
||||
set:
|
||||
is_admin: true
|
||||
|
||||
- type: create_select_permission
|
||||
args:
|
||||
table: user
|
||||
role: backend_user
|
||||
permission:
|
||||
columns: '*'
|
||||
filter: {}
|
||||
|
||||
- type: create_insert_permission
|
||||
args:
|
||||
table: user
|
||||
role: user
|
||||
permission:
|
||||
check: {}
|
||||
columns: '*'
|
||||
backend_only: false
|
||||
set:
|
||||
is_admin: false
|
||||
|
@ -0,0 +1,24 @@
|
||||
description: As user with no backend privilege
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
headers:
|
||||
X-Hasura-Role: user
|
||||
X-Hasura-Use-Backend-Only-Permissions: 'true'
|
||||
response:
|
||||
errors:
|
||||
- extensions:
|
||||
path: $.selectionSet.insert_user
|
||||
code: validation-failed
|
||||
message: "field \"insert_user\" not found in type: 'mutation_root'"
|
||||
|
||||
query:
|
||||
query: |
|
||||
mutation {
|
||||
insert_user(objects: [
|
||||
{
|
||||
name: "FooBar"
|
||||
}
|
||||
]){
|
||||
affected_rows
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import pytest
|
||||
from validate import check_query_f
|
||||
from validate import check_query_f, check_query, get_conf_f
|
||||
|
||||
|
||||
# Marking all tests in this module that server upgrade tests can be run
|
||||
# Few of them cannot be run, which will be marked skip_server_upgrade_test
|
||||
@ -173,10 +174,35 @@ class TestGraphqlInsertPermission:
|
||||
def test_user_insert_account_fail(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + "/user_insert_account_fail.yaml")
|
||||
|
||||
def test_backend_user_insert_fail(self, hge_ctx):
|
||||
check_query_admin_secret(hge_ctx, self.dir() + "/backend_user_insert_fail.yaml")
|
||||
|
||||
def test_backend_user_insert_pass(self, hge_ctx):
|
||||
check_query_admin_secret(hge_ctx, self.dir() + "/backend_user_insert_pass.yaml")
|
||||
|
||||
def test_backend_user_insert_invalid_bool(self, hge_ctx):
|
||||
check_query_admin_secret(hge_ctx, self.dir() + "/backend_user_insert_invalid_bool.yaml")
|
||||
|
||||
def test_user_with_no_backend_privilege(self, hge_ctx):
|
||||
check_query_admin_secret(hge_ctx, self.dir() + "/user_with_no_backend_privilege.yaml")
|
||||
|
||||
def test_backend_user_no_admin_secret_fail(self, hge_ctx):
|
||||
if hge_ctx.hge_key and (hge_ctx.hge_jwt_key or hge_ctx.hge_webhook):
|
||||
check_query_f(hge_ctx, self.dir() + "/backend_user_no_admin_secret_fail.yaml")
|
||||
else:
|
||||
pytest.skip("authorization not configured, skipping the test")
|
||||
|
||||
@classmethod
|
||||
def dir(cls):
|
||||
return "queries/graphql_mutation/insert/permissions"
|
||||
|
||||
def check_query_admin_secret(hge_ctx, f, transport='http'):
|
||||
conf = get_conf_f(f)
|
||||
admin_secret = hge_ctx.hge_key
|
||||
if admin_secret:
|
||||
conf['headers']['x-hasura-admin-secret'] = admin_secret
|
||||
check_query(hge_ctx, conf, transport, False)
|
||||
|
||||
|
||||
@usefixtures('per_class_tests_db_state')
|
||||
class TestGraphqlInsertConstraints:
|
||||
|
Loading…
Reference in New Issue
Block a user