2020-04-24 10:55:51 +03:00
|
|
|
-- | Types and classes related to configuration when the server is initialised
|
|
|
|
module Hasura.Server.Init.Config where
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
import Data.Aeson
|
|
|
|
import Data.Aeson qualified as J
|
|
|
|
import Data.Aeson.Casing qualified as J
|
|
|
|
import Data.Aeson.TH qualified as J
|
|
|
|
import Data.Char (toLower)
|
|
|
|
import Data.HashSet qualified as Set
|
|
|
|
import Data.String qualified as DataString
|
|
|
|
import Data.Text qualified as T
|
|
|
|
import Data.Time
|
|
|
|
import Data.URL.Template
|
|
|
|
import Database.PG.Query qualified as Q
|
|
|
|
import Hasura.GraphQL.Execute.LiveQuery.Options qualified as LQ
|
|
|
|
import Hasura.Logging qualified as L
|
|
|
|
import Hasura.Prelude
|
|
|
|
import Hasura.RQL.Types
|
|
|
|
import Hasura.Server.Auth
|
|
|
|
import Hasura.Server.Cors
|
|
|
|
import Hasura.Server.Types
|
|
|
|
import Hasura.Server.Utils
|
|
|
|
import Hasura.Session
|
|
|
|
import Network.Wai.Handler.Warp (HostPreference)
|
|
|
|
import Network.WebSockets qualified as WS
|
|
|
|
|
|
|
|
data RawConnParams = RawConnParams
|
|
|
|
{ rcpStripes :: !(Maybe Int),
|
|
|
|
rcpConns :: !(Maybe Int),
|
|
|
|
rcpIdleTime :: !(Maybe Int),
|
|
|
|
-- | Time from connection creation after which to destroy a connection and
|
|
|
|
-- choose a different/new one.
|
|
|
|
rcpConnLifetime :: !(Maybe NominalDiffTime),
|
|
|
|
rcpAllowPrepare :: !(Maybe Bool),
|
|
|
|
-- | See @HASURA_GRAPHQL_PG_POOL_TIMEOUT@
|
|
|
|
rcpPoolTimeout :: !(Maybe NominalDiffTime)
|
|
|
|
}
|
|
|
|
deriving (Show, Eq)
|
2020-04-24 10:55:51 +03:00
|
|
|
|
2020-10-27 16:53:49 +03:00
|
|
|
type RawAuthHook = AuthHookG (Maybe Text) (Maybe AuthHookType)
|
2020-04-24 10:55:51 +03:00
|
|
|
|
2021-04-07 12:59:48 +03:00
|
|
|
-- | Sleep time interval for recurring activities such as (@'asyncActionsProcessor')
|
|
|
|
-- Presently @'msToOptionalInterval' interprets `0` as Skip.
|
|
|
|
data OptionalInterval
|
2021-09-24 01:56:37 +03:00
|
|
|
= -- | No polling
|
|
|
|
Skip
|
|
|
|
| -- | Interval time
|
|
|
|
Interval !Milliseconds
|
2021-03-31 13:39:01 +03:00
|
|
|
deriving (Show, Eq)
|
|
|
|
|
2021-04-07 12:59:48 +03:00
|
|
|
msToOptionalInterval :: Milliseconds -> OptionalInterval
|
|
|
|
msToOptionalInterval = \case
|
|
|
|
0 -> Skip
|
|
|
|
s -> Interval s
|
2021-03-31 13:39:01 +03:00
|
|
|
|
2021-04-07 12:59:48 +03:00
|
|
|
instance FromJSON OptionalInterval where
|
|
|
|
parseJSON v = msToOptionalInterval <$> parseJSON v
|
2021-03-31 13:39:01 +03:00
|
|
|
|
2021-04-07 12:59:48 +03:00
|
|
|
instance ToJSON OptionalInterval where
|
2021-03-31 13:39:01 +03:00
|
|
|
toJSON = \case
|
2021-09-24 01:56:37 +03:00
|
|
|
Skip -> toJSON @Milliseconds 0
|
2021-04-07 12:59:48 +03:00
|
|
|
Interval s -> toJSON s
|
2021-03-31 13:39:01 +03:00
|
|
|
|
2021-08-24 19:25:12 +03:00
|
|
|
data API
|
|
|
|
= METADATA
|
|
|
|
| GRAPHQL
|
|
|
|
| PGDUMP
|
|
|
|
| DEVELOPER
|
|
|
|
| CONFIG
|
|
|
|
deriving (Show, Eq, Read, Generic)
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
$( J.deriveJSON
|
|
|
|
(J.defaultOptions {J.constructorTagModifier = map toLower})
|
|
|
|
''API
|
|
|
|
)
|
2021-08-24 19:25:12 +03:00
|
|
|
|
|
|
|
instance Hashable API
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
data RawServeOptions impl = RawServeOptions
|
|
|
|
{ rsoPort :: !(Maybe Int),
|
|
|
|
rsoHost :: !(Maybe HostPreference),
|
|
|
|
rsoConnParams :: !RawConnParams,
|
|
|
|
rsoTxIso :: !(Maybe Q.TxIsolation),
|
|
|
|
rsoAdminSecret :: !(Maybe AdminSecretHash),
|
|
|
|
rsoAuthHook :: !RawAuthHook,
|
|
|
|
rsoJwtSecret :: !(Maybe JWTConfig),
|
|
|
|
rsoUnAuthRole :: !(Maybe RoleName),
|
|
|
|
rsoCorsConfig :: !(Maybe CorsConfig),
|
|
|
|
rsoEnableConsole :: !Bool,
|
|
|
|
rsoConsoleAssetsDir :: !(Maybe Text),
|
|
|
|
rsoEnableTelemetry :: !(Maybe Bool),
|
|
|
|
rsoWsReadCookie :: !Bool,
|
|
|
|
rsoStringifyNum :: !Bool,
|
|
|
|
rsoDangerousBooleanCollapse :: !(Maybe Bool),
|
|
|
|
rsoEnabledAPIs :: !(Maybe [API]),
|
|
|
|
rsoMxRefetchInt :: !(Maybe LQ.RefetchInterval),
|
|
|
|
rsoMxBatchSize :: !(Maybe LQ.BatchSize),
|
|
|
|
rsoEnableAllowlist :: !Bool,
|
|
|
|
rsoEnabledLogTypes :: !(Maybe [L.EngineLogType impl]),
|
|
|
|
rsoLogLevel :: !(Maybe L.LogLevel),
|
|
|
|
rsoDevMode :: !Bool,
|
|
|
|
rsoAdminInternalErrors :: !(Maybe Bool),
|
|
|
|
rsoEventsHttpPoolSize :: !(Maybe Int),
|
|
|
|
rsoEventsFetchInterval :: !(Maybe Milliseconds),
|
|
|
|
rsoAsyncActionsFetchInterval :: !(Maybe Milliseconds),
|
|
|
|
rsoLogHeadersFromEnv :: !Bool,
|
|
|
|
rsoEnableRemoteSchemaPermissions :: !Bool,
|
|
|
|
rsoWebSocketCompression :: !Bool,
|
|
|
|
rsoWebSocketKeepAlive :: !(Maybe Int),
|
|
|
|
rsoInferFunctionPermissions :: !(Maybe Bool),
|
|
|
|
rsoEnableMaintenanceMode :: !Bool,
|
|
|
|
rsoSchemaPollInterval :: !(Maybe Milliseconds),
|
|
|
|
rsoExperimentalFeatures :: !(Maybe [ExperimentalFeature]),
|
|
|
|
rsoEventsFetchBatchSize :: !(Maybe NonNegativeInt),
|
|
|
|
rsoGracefulShutdownTimeout :: !(Maybe Seconds),
|
|
|
|
rsoWebSocketConnectionInitTimeout :: !(Maybe Int)
|
2020-04-24 10:55:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
-- | @'ResponseInternalErrorsConfig' represents the encoding of the internal
|
|
|
|
-- errors in the response to the client.
|
|
|
|
-- See the github comment https://github.com/hasura/graphql-engine/issues/4031#issuecomment-609747705 for more details.
|
|
|
|
data ResponseInternalErrorsConfig
|
|
|
|
= InternalErrorsAllRequests
|
|
|
|
| InternalErrorsAdminOnly
|
|
|
|
| InternalErrorsDisabled
|
|
|
|
deriving (Show, Eq)
|
|
|
|
|
|
|
|
shouldIncludeInternal :: RoleName -> ResponseInternalErrorsConfig -> Bool
|
|
|
|
shouldIncludeInternal role = \case
|
|
|
|
InternalErrorsAllRequests -> True
|
2021-09-24 01:56:37 +03:00
|
|
|
InternalErrorsAdminOnly -> role == adminRoleName
|
|
|
|
InternalErrorsDisabled -> False
|
2020-04-24 10:55:51 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
newtype KeepAliveDelay = KeepAliveDelay {unKeepAliveDelay :: Seconds}
|
2021-08-24 19:25:12 +03:00
|
|
|
deriving (Eq, Show)
|
2021-09-24 01:56:37 +03:00
|
|
|
|
2021-08-24 19:25:12 +03:00
|
|
|
$(J.deriveJSON hasuraJSON ''KeepAliveDelay)
|
|
|
|
|
|
|
|
defaultKeepAliveDelay :: KeepAliveDelay
|
|
|
|
defaultKeepAliveDelay = KeepAliveDelay $ fromIntegral (5 :: Int)
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
newtype WSConnectionInitTimeout = WSConnectionInitTimeout {unWSConnectionInitTimeout :: Seconds}
|
2021-08-24 19:25:12 +03:00
|
|
|
deriving (Eq, Show)
|
2021-09-24 01:56:37 +03:00
|
|
|
|
2021-08-24 19:25:12 +03:00
|
|
|
$(J.deriveJSON hasuraJSON ''WSConnectionInitTimeout)
|
|
|
|
|
|
|
|
defaultWSConnectionInitTimeout :: WSConnectionInitTimeout
|
|
|
|
defaultWSConnectionInitTimeout = WSConnectionInitTimeout $ fromIntegral (3 :: Int)
|
2020-11-12 12:25:48 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
data ServeOptions impl = ServeOptions
|
|
|
|
{ soPort :: !Int,
|
|
|
|
soHost :: !HostPreference,
|
|
|
|
soConnParams :: !Q.ConnParams,
|
|
|
|
soTxIso :: !Q.TxIsolation,
|
|
|
|
soAdminSecret :: !(Maybe AdminSecretHash),
|
|
|
|
soAuthHook :: !(Maybe AuthHook),
|
|
|
|
soJwtSecret :: !(Maybe JWTConfig),
|
|
|
|
soUnAuthRole :: !(Maybe RoleName),
|
|
|
|
soCorsConfig :: !CorsConfig,
|
|
|
|
soEnableConsole :: !Bool,
|
|
|
|
soConsoleAssetsDir :: !(Maybe Text),
|
|
|
|
soEnableTelemetry :: !Bool,
|
|
|
|
soStringifyNum :: !Bool,
|
|
|
|
soDangerousBooleanCollapse :: !Bool,
|
|
|
|
soEnabledAPIs :: !(Set.HashSet API),
|
|
|
|
soLiveQueryOpts :: !LQ.LiveQueriesOptions,
|
|
|
|
soEnableAllowlist :: !Bool,
|
|
|
|
soEnabledLogTypes :: !(Set.HashSet (L.EngineLogType impl)),
|
|
|
|
soLogLevel :: !L.LogLevel,
|
|
|
|
soResponseInternalErrorsConfig :: !ResponseInternalErrorsConfig,
|
|
|
|
soEventsHttpPoolSize :: !(Maybe Int),
|
|
|
|
soEventsFetchInterval :: !(Maybe Milliseconds),
|
|
|
|
soAsyncActionsFetchInterval :: !OptionalInterval,
|
|
|
|
soLogHeadersFromEnv :: !Bool,
|
|
|
|
soEnableRemoteSchemaPermissions :: !RemoteSchemaPermsCtx,
|
|
|
|
soConnectionOptions :: !WS.ConnectionOptions,
|
|
|
|
soWebsocketKeepAlive :: !KeepAliveDelay,
|
|
|
|
soInferFunctionPermissions :: !FunctionPermissionsCtx,
|
|
|
|
soEnableMaintenanceMode :: !MaintenanceMode,
|
|
|
|
soSchemaPollInterval :: !OptionalInterval,
|
|
|
|
soExperimentalFeatures :: !(Set.HashSet ExperimentalFeature),
|
|
|
|
soEventsFetchBatchSize :: !NonNegativeInt,
|
|
|
|
soDevMode :: !Bool,
|
|
|
|
soGracefulShutdownTimeout :: !Seconds,
|
|
|
|
soWebsocketConnectionInitTimeout :: !WSConnectionInitTimeout
|
2020-04-24 10:55:51 +03:00
|
|
|
}
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
data DowngradeOptions = DowngradeOptions
|
|
|
|
{ dgoTargetVersion :: !Text,
|
|
|
|
dgoDryRun :: !Bool
|
|
|
|
}
|
|
|
|
deriving (Show, Eq)
|
|
|
|
|
|
|
|
data PostgresConnInfo a = PostgresConnInfo
|
|
|
|
{ _pciDatabaseConn :: !a,
|
|
|
|
_pciRetries :: !(Maybe Int)
|
|
|
|
}
|
|
|
|
deriving (Show, Eq, Functor, Foldable, Traversable)
|
|
|
|
|
|
|
|
data PostgresRawConnDetails = PostgresRawConnDetails
|
|
|
|
{ connHost :: !String,
|
|
|
|
connPort :: !Int,
|
|
|
|
connUser :: !String,
|
|
|
|
connPassword :: !String,
|
|
|
|
connDatabase :: !String,
|
|
|
|
connOptions :: !(Maybe String)
|
|
|
|
}
|
|
|
|
deriving (Eq, Read, Show)
|
2020-04-24 10:55:51 +03:00
|
|
|
|
2020-12-28 15:56:00 +03:00
|
|
|
data PostgresRawConnInfo
|
|
|
|
= PGConnDatabaseUrl !URLTemplate
|
|
|
|
| PGConnDetails !PostgresRawConnDetails
|
|
|
|
deriving (Show, Eq)
|
|
|
|
|
|
|
|
rawConnDetailsToUrl :: PostgresRawConnDetails -> URLTemplate
|
|
|
|
rawConnDetailsToUrl =
|
|
|
|
mkPlainURLTemplate . rawConnDetailsToUrlText
|
|
|
|
|
|
|
|
rawConnDetailsToUrlText :: PostgresRawConnDetails -> Text
|
2021-09-24 01:56:37 +03:00
|
|
|
rawConnDetailsToUrlText PostgresRawConnDetails {..} =
|
2020-12-28 15:56:00 +03:00
|
|
|
T.pack $
|
2021-09-24 01:56:37 +03:00
|
|
|
"postgresql://" <> connUser
|
|
|
|
<> ":"
|
|
|
|
<> connPassword
|
|
|
|
<> "@"
|
|
|
|
<> connHost
|
|
|
|
<> ":"
|
|
|
|
<> show connPort
|
|
|
|
<> "/"
|
|
|
|
<> connDatabase
|
|
|
|
<> maybe "" ("?options=" <>) connOptions
|
2020-12-28 15:56:00 +03:00
|
|
|
|
2020-04-24 10:55:51 +03:00
|
|
|
data HGECommandG a
|
|
|
|
= HCServe !a
|
|
|
|
| HCExport
|
|
|
|
| HCClean
|
|
|
|
| HCVersion
|
|
|
|
| HCDowngrade !DowngradeOptions
|
|
|
|
deriving (Show, Eq)
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
$(J.deriveJSON (J.aesonPrefix J.camelCase) {J.omitNothingFields = True} ''PostgresRawConnDetails)
|
2020-07-14 22:00:58 +03:00
|
|
|
|
2020-04-24 10:55:51 +03:00
|
|
|
type HGECommand impl = HGECommandG (ServeOptions impl)
|
2021-09-24 01:56:37 +03:00
|
|
|
|
2020-04-24 10:55:51 +03:00
|
|
|
type RawHGECommand impl = HGECommandG (RawServeOptions impl)
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
data HGEOptionsG a b = HGEOptionsG
|
|
|
|
{ hoDatabaseUrl :: !(PostgresConnInfo a),
|
|
|
|
hoMetadataDbUrl :: !(Maybe String),
|
|
|
|
hoCommand :: !(HGECommandG b)
|
|
|
|
}
|
|
|
|
deriving (Show, Eq)
|
2020-04-24 10:55:51 +03:00
|
|
|
|
2020-12-28 15:56:00 +03:00
|
|
|
type RawHGEOptions impl = HGEOptionsG (Maybe PostgresRawConnInfo) (RawServeOptions impl)
|
2021-09-24 01:56:37 +03:00
|
|
|
|
2021-01-07 12:04:22 +03:00
|
|
|
type HGEOptions impl = HGEOptionsG (Maybe UrlConf) (ServeOptions impl)
|
2020-04-24 10:55:51 +03:00
|
|
|
|
|
|
|
type Env = [(String, String)]
|
|
|
|
|
|
|
|
readHookType :: String -> Either String AuthHookType
|
|
|
|
readHookType tyS =
|
|
|
|
case tyS of
|
2021-09-24 01:56:37 +03:00
|
|
|
"GET" -> Right AHTGet
|
2020-04-24 10:55:51 +03:00
|
|
|
"POST" -> Right AHTPost
|
2021-09-24 01:56:37 +03:00
|
|
|
_ -> Left "Only expecting GET / POST"
|
2020-04-24 10:55:51 +03:00
|
|
|
|
|
|
|
parseStrAsBool :: String -> Either String Bool
|
|
|
|
parseStrAsBool 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"]
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
errMsg =
|
|
|
|
" Not a valid boolean text. " ++ "True values are "
|
|
|
|
++ show truthVals
|
|
|
|
++ " and False values are "
|
|
|
|
++ show falseVals
|
|
|
|
++ ". All values are case insensitive"
|
2020-04-24 10:55:51 +03:00
|
|
|
|
2021-04-27 20:22:54 +03:00
|
|
|
readNonNegativeInt :: String -> Either String NonNegativeInt
|
|
|
|
readNonNegativeInt s =
|
|
|
|
onNothing (mkNonNegativeInt =<< readMaybe s) $ Left "Only expecting a non negative integer"
|
|
|
|
|
2020-04-24 10:55:51 +03:00
|
|
|
readAPIs :: String -> Either String [API]
|
|
|
|
readAPIs = mapM readAPI . T.splitOn "," . T.pack
|
2021-09-24 01:56:37 +03:00
|
|
|
where
|
|
|
|
readAPI si = case T.toUpper $ T.strip si of
|
|
|
|
"METADATA" -> Right METADATA
|
|
|
|
"GRAPHQL" -> Right GRAPHQL
|
|
|
|
"PGDUMP" -> Right PGDUMP
|
|
|
|
"DEVELOPER" -> Right DEVELOPER
|
|
|
|
"CONFIG" -> Right CONFIG
|
|
|
|
_ -> Left "Only expecting list of comma separated API types metadata,graphql,pgdump,developer,config"
|
2020-04-24 10:55:51 +03:00
|
|
|
|
[Preview] Inherited roles for postgres read queries
fixes #3868
docker image - `hasura/graphql-engine:inherited-roles-preview-48b73a2de`
Note:
To be able to use the inherited roles feature, the graphql-engine should be started with the env variable `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` set to `inherited_roles`.
Introduction
------------
This PR implements the idea of multiple roles as presented in this [paper](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/FGALanguageICDE07.pdf). The multiple roles feature in this PR can be used via inherited roles. An inherited role is a role which can be created by combining multiple singular roles. For example, if there are two roles `author` and `editor` configured in the graphql-engine, then we can create a inherited role with the name of `combined_author_editor` role which will combine the select permissions of the `author` and `editor` roles and then make GraphQL queries using the `combined_author_editor`.
How are select permissions of different roles are combined?
------------------------------------------------------------
A select permission includes 5 things:
1. Columns accessible to the role
2. Row selection filter
3. Limit
4. Allow aggregation
5. Scalar computed fields accessible to the role
Suppose there are two roles, `role1` gives access to the `address` column with row filter `P1` and `role2` gives access to both the `address` and the `phone` column with row filter `P2` and we create a new role `combined_roles` which combines `role1` and `role2`.
Let's say the following GraphQL query is queried with the `combined_roles` role.
```graphql
query {
employees {
address
phone
}
}
```
This will translate to the following SQL query:
```sql
select
(case when (P1 or P2) then address else null end) as address,
(case when P2 then phone else null end) as phone
from employee
where (P1 or P2)
```
The other parameters of the select permission will be combined in the following manner:
1. Limit - Minimum of the limits will be the limit of the inherited role
2. Allow aggregations - If any of the role allows aggregation, then the inherited role will allow aggregation
3. Scalar computed fields - same as table column fields, as in the above example
APIs for inherited roles:
----------------------
1. `add_inherited_role`
`add_inherited_role` is the [metadata API](https://hasura.io/docs/1.0/graphql/core/api-reference/index.html#schema-metadata-api) to create a new inherited role. It accepts two arguments
`role_name`: the name of the inherited role to be added (String)
`role_set`: list of roles that need to be combined (Array of Strings)
Example:
```json
{
"type": "add_inherited_role",
"args": {
"role_name":"combined_user",
"role_set":[
"user",
"user1"
]
}
}
```
After adding the inherited role, the inherited role can be used like single roles like earlier
Note:
An inherited role can only be created with non-inherited/singular roles.
2. `drop_inherited_role`
The `drop_inherited_role` API accepts the name of the inherited role and drops it from the metadata. It accepts a single argument:
`role_name`: name of the inherited role to be dropped
Example:
```json
{
"type": "drop_inherited_role",
"args": {
"role_name":"combined_user"
}
}
```
Metadata
---------
The derived roles metadata will be included under the `experimental_features` key while exporting the metadata.
```json
{
"experimental_features": {
"derived_roles": [
{
"role_name": "manager_is_employee_too",
"role_set": [
"employee",
"manager"
]
}
]
}
}
```
Scope
------
Only postgres queries and subscriptions are supported in this PR.
Important points:
-----------------
1. All columns exposed to an inherited role will be marked as `nullable`, this is done so that cell value nullification can be done.
TODOs
-------
- [ ] Tests
- [ ] Test a GraphQL query running with a inherited role without enabling inherited roles in experimental features
- [] Tests for aggregate queries, limit, computed fields, functions, subscriptions (?)
- [ ] Introspection test with a inherited role (nullability changes in a inherited role)
- [ ] Docs
- [ ] Changelog
Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com>
GitOrigin-RevId: 3b8ee1e11f5ceca80fe294f8c074d42fbccfec63
2021-03-08 14:14:13 +03:00
|
|
|
readExperimentalFeatures :: String -> Either String [ExperimentalFeature]
|
|
|
|
readExperimentalFeatures = mapM readAPI . T.splitOn "," . T.pack
|
2021-09-24 01:56:37 +03:00
|
|
|
where
|
|
|
|
readAPI si = case T.toLower $ T.strip si of
|
|
|
|
"inherited_roles" -> Right EFInheritedRoles
|
|
|
|
_ -> Left "Only expecting list of comma separated experimental features"
|
[Preview] Inherited roles for postgres read queries
fixes #3868
docker image - `hasura/graphql-engine:inherited-roles-preview-48b73a2de`
Note:
To be able to use the inherited roles feature, the graphql-engine should be started with the env variable `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` set to `inherited_roles`.
Introduction
------------
This PR implements the idea of multiple roles as presented in this [paper](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/FGALanguageICDE07.pdf). The multiple roles feature in this PR can be used via inherited roles. An inherited role is a role which can be created by combining multiple singular roles. For example, if there are two roles `author` and `editor` configured in the graphql-engine, then we can create a inherited role with the name of `combined_author_editor` role which will combine the select permissions of the `author` and `editor` roles and then make GraphQL queries using the `combined_author_editor`.
How are select permissions of different roles are combined?
------------------------------------------------------------
A select permission includes 5 things:
1. Columns accessible to the role
2. Row selection filter
3. Limit
4. Allow aggregation
5. Scalar computed fields accessible to the role
Suppose there are two roles, `role1` gives access to the `address` column with row filter `P1` and `role2` gives access to both the `address` and the `phone` column with row filter `P2` and we create a new role `combined_roles` which combines `role1` and `role2`.
Let's say the following GraphQL query is queried with the `combined_roles` role.
```graphql
query {
employees {
address
phone
}
}
```
This will translate to the following SQL query:
```sql
select
(case when (P1 or P2) then address else null end) as address,
(case when P2 then phone else null end) as phone
from employee
where (P1 or P2)
```
The other parameters of the select permission will be combined in the following manner:
1. Limit - Minimum of the limits will be the limit of the inherited role
2. Allow aggregations - If any of the role allows aggregation, then the inherited role will allow aggregation
3. Scalar computed fields - same as table column fields, as in the above example
APIs for inherited roles:
----------------------
1. `add_inherited_role`
`add_inherited_role` is the [metadata API](https://hasura.io/docs/1.0/graphql/core/api-reference/index.html#schema-metadata-api) to create a new inherited role. It accepts two arguments
`role_name`: the name of the inherited role to be added (String)
`role_set`: list of roles that need to be combined (Array of Strings)
Example:
```json
{
"type": "add_inherited_role",
"args": {
"role_name":"combined_user",
"role_set":[
"user",
"user1"
]
}
}
```
After adding the inherited role, the inherited role can be used like single roles like earlier
Note:
An inherited role can only be created with non-inherited/singular roles.
2. `drop_inherited_role`
The `drop_inherited_role` API accepts the name of the inherited role and drops it from the metadata. It accepts a single argument:
`role_name`: name of the inherited role to be dropped
Example:
```json
{
"type": "drop_inherited_role",
"args": {
"role_name":"combined_user"
}
}
```
Metadata
---------
The derived roles metadata will be included under the `experimental_features` key while exporting the metadata.
```json
{
"experimental_features": {
"derived_roles": [
{
"role_name": "manager_is_employee_too",
"role_set": [
"employee",
"manager"
]
}
]
}
}
```
Scope
------
Only postgres queries and subscriptions are supported in this PR.
Important points:
-----------------
1. All columns exposed to an inherited role will be marked as `nullable`, this is done so that cell value nullification can be done.
TODOs
-------
- [ ] Tests
- [ ] Test a GraphQL query running with a inherited role without enabling inherited roles in experimental features
- [] Tests for aggregate queries, limit, computed fields, functions, subscriptions (?)
- [ ] Introspection test with a inherited role (nullability changes in a inherited role)
- [ ] Docs
- [ ] Changelog
Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com>
GitOrigin-RevId: 3b8ee1e11f5ceca80fe294f8c074d42fbccfec63
2021-03-08 14:14:13 +03:00
|
|
|
|
2020-04-24 10:55:51 +03:00
|
|
|
readLogLevel :: String -> Either String L.LogLevel
|
|
|
|
readLogLevel s = case T.toLower $ T.strip $ T.pack s of
|
|
|
|
"debug" -> Right L.LevelDebug
|
2021-09-24 01:56:37 +03:00
|
|
|
"info" -> Right L.LevelInfo
|
|
|
|
"warn" -> Right L.LevelWarn
|
2020-04-24 10:55:51 +03:00
|
|
|
"error" -> Right L.LevelError
|
2021-09-24 01:56:37 +03:00
|
|
|
_ -> Left "Valid log levels: debug, info, warn or error"
|
2020-04-24 10:55:51 +03:00
|
|
|
|
|
|
|
readJson :: (J.FromJSON a) => String -> Either String a
|
|
|
|
readJson = J.eitherDecodeStrict . txtToBs . T.pack
|
|
|
|
|
|
|
|
class FromEnv a where
|
|
|
|
fromEnv :: String -> Either String a
|
|
|
|
|
2020-07-01 06:53:10 +03:00
|
|
|
-- Deserialize from seconds, in the usual way
|
|
|
|
instance FromEnv NominalDiffTime where
|
2021-09-24 01:56:37 +03:00
|
|
|
fromEnv s =
|
|
|
|
maybe (Left "could not parse as a Double") (Right . realToFrac) $
|
|
|
|
(readMaybe s :: Maybe Double)
|
2020-07-01 06:53:10 +03:00
|
|
|
|
2020-04-24 10:55:51 +03:00
|
|
|
instance FromEnv String where
|
|
|
|
fromEnv = Right
|
|
|
|
|
|
|
|
instance FromEnv HostPreference where
|
|
|
|
fromEnv = Right . DataString.fromString
|
|
|
|
|
|
|
|
instance FromEnv Text where
|
|
|
|
fromEnv = Right . T.pack
|
|
|
|
|
|
|
|
instance FromEnv AuthHookType where
|
|
|
|
fromEnv = readHookType
|
|
|
|
|
|
|
|
instance FromEnv Int where
|
|
|
|
fromEnv = maybe (Left "Expecting Int value") Right . readMaybe
|
|
|
|
|
2020-05-19 17:48:49 +03:00
|
|
|
instance FromEnv AdminSecretHash where
|
|
|
|
fromEnv = Right . hashAdminSecret . T.pack
|
2020-04-24 10:55:51 +03:00
|
|
|
|
|
|
|
instance FromEnv RoleName where
|
2020-04-24 12:10:53 +03:00
|
|
|
fromEnv string = case mkRoleName (T.pack string) of
|
2021-09-24 01:56:37 +03:00
|
|
|
Nothing -> Left "empty string not allowed"
|
2020-04-24 12:10:53 +03:00
|
|
|
Just roleName -> Right roleName
|
2020-04-24 10:55:51 +03:00
|
|
|
|
|
|
|
instance FromEnv Bool where
|
|
|
|
fromEnv = parseStrAsBool
|
|
|
|
|
|
|
|
instance FromEnv Q.TxIsolation where
|
|
|
|
fromEnv = readIsoLevel
|
|
|
|
|
|
|
|
instance FromEnv CorsConfig where
|
|
|
|
fromEnv = readCorsDomains
|
|
|
|
|
|
|
|
instance FromEnv [API] where
|
|
|
|
fromEnv = readAPIs
|
|
|
|
|
[Preview] Inherited roles for postgres read queries
fixes #3868
docker image - `hasura/graphql-engine:inherited-roles-preview-48b73a2de`
Note:
To be able to use the inherited roles feature, the graphql-engine should be started with the env variable `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` set to `inherited_roles`.
Introduction
------------
This PR implements the idea of multiple roles as presented in this [paper](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/FGALanguageICDE07.pdf). The multiple roles feature in this PR can be used via inherited roles. An inherited role is a role which can be created by combining multiple singular roles. For example, if there are two roles `author` and `editor` configured in the graphql-engine, then we can create a inherited role with the name of `combined_author_editor` role which will combine the select permissions of the `author` and `editor` roles and then make GraphQL queries using the `combined_author_editor`.
How are select permissions of different roles are combined?
------------------------------------------------------------
A select permission includes 5 things:
1. Columns accessible to the role
2. Row selection filter
3. Limit
4. Allow aggregation
5. Scalar computed fields accessible to the role
Suppose there are two roles, `role1` gives access to the `address` column with row filter `P1` and `role2` gives access to both the `address` and the `phone` column with row filter `P2` and we create a new role `combined_roles` which combines `role1` and `role2`.
Let's say the following GraphQL query is queried with the `combined_roles` role.
```graphql
query {
employees {
address
phone
}
}
```
This will translate to the following SQL query:
```sql
select
(case when (P1 or P2) then address else null end) as address,
(case when P2 then phone else null end) as phone
from employee
where (P1 or P2)
```
The other parameters of the select permission will be combined in the following manner:
1. Limit - Minimum of the limits will be the limit of the inherited role
2. Allow aggregations - If any of the role allows aggregation, then the inherited role will allow aggregation
3. Scalar computed fields - same as table column fields, as in the above example
APIs for inherited roles:
----------------------
1. `add_inherited_role`
`add_inherited_role` is the [metadata API](https://hasura.io/docs/1.0/graphql/core/api-reference/index.html#schema-metadata-api) to create a new inherited role. It accepts two arguments
`role_name`: the name of the inherited role to be added (String)
`role_set`: list of roles that need to be combined (Array of Strings)
Example:
```json
{
"type": "add_inherited_role",
"args": {
"role_name":"combined_user",
"role_set":[
"user",
"user1"
]
}
}
```
After adding the inherited role, the inherited role can be used like single roles like earlier
Note:
An inherited role can only be created with non-inherited/singular roles.
2. `drop_inherited_role`
The `drop_inherited_role` API accepts the name of the inherited role and drops it from the metadata. It accepts a single argument:
`role_name`: name of the inherited role to be dropped
Example:
```json
{
"type": "drop_inherited_role",
"args": {
"role_name":"combined_user"
}
}
```
Metadata
---------
The derived roles metadata will be included under the `experimental_features` key while exporting the metadata.
```json
{
"experimental_features": {
"derived_roles": [
{
"role_name": "manager_is_employee_too",
"role_set": [
"employee",
"manager"
]
}
]
}
}
```
Scope
------
Only postgres queries and subscriptions are supported in this PR.
Important points:
-----------------
1. All columns exposed to an inherited role will be marked as `nullable`, this is done so that cell value nullification can be done.
TODOs
-------
- [ ] Tests
- [ ] Test a GraphQL query running with a inherited role without enabling inherited roles in experimental features
- [] Tests for aggregate queries, limit, computed fields, functions, subscriptions (?)
- [ ] Introspection test with a inherited role (nullability changes in a inherited role)
- [ ] Docs
- [ ] Changelog
Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com>
GitOrigin-RevId: 3b8ee1e11f5ceca80fe294f8c074d42fbccfec63
2021-03-08 14:14:13 +03:00
|
|
|
instance FromEnv [ExperimentalFeature] where
|
|
|
|
fromEnv = readExperimentalFeatures
|
|
|
|
|
2020-04-24 10:55:51 +03:00
|
|
|
instance FromEnv LQ.BatchSize where
|
2020-09-17 13:56:41 +03:00
|
|
|
fromEnv s = do
|
|
|
|
val <- readEither s
|
|
|
|
maybe (Left "batch size should be a non negative integer") Right $ LQ.mkBatchSize val
|
2020-04-24 10:55:51 +03:00
|
|
|
|
|
|
|
instance FromEnv LQ.RefetchInterval where
|
2020-09-17 13:56:41 +03:00
|
|
|
fromEnv x = do
|
|
|
|
val <- fmap (milliseconds . fromInteger) . readEither $ x
|
|
|
|
maybe (Left "refetch interval should be a non negative integer") Right $ LQ.mkRefetchInterval val
|
2020-04-24 10:55:51 +03:00
|
|
|
|
2020-07-14 22:00:58 +03:00
|
|
|
instance FromEnv Milliseconds where
|
|
|
|
fromEnv = fmap fromInteger . readEither
|
|
|
|
|
2021-05-14 12:38:37 +03:00
|
|
|
instance FromEnv Seconds where
|
|
|
|
fromEnv = fmap fromInteger . readEither
|
|
|
|
|
2020-04-24 10:55:51 +03:00
|
|
|
instance FromEnv JWTConfig where
|
|
|
|
fromEnv = readJson
|
|
|
|
|
|
|
|
instance L.EnabledLogTypes impl => FromEnv [L.EngineLogType impl] where
|
|
|
|
fromEnv = L.parseEnabledLogTypes
|
|
|
|
|
|
|
|
instance FromEnv L.LogLevel where
|
|
|
|
fromEnv = readLogLevel
|
|
|
|
|
2020-12-28 15:56:00 +03:00
|
|
|
instance FromEnv URLTemplate where
|
|
|
|
fromEnv = parseURLTemplate . T.pack
|
|
|
|
|
2021-04-27 20:22:54 +03:00
|
|
|
instance FromEnv NonNegativeInt where
|
|
|
|
fromEnv = readNonNegativeInt
|
|
|
|
|
2020-04-24 10:55:51 +03:00
|
|
|
type WithEnv a = ReaderT Env (ExceptT String Identity) a
|
|
|
|
|
|
|
|
runWithEnv :: Env -> WithEnv a -> Either String a
|
|
|
|
runWithEnv env m = runIdentity $ runExceptT $ runReaderT m env
|