add support for argument presets in commands (#340)

V3_GIT_ORIGIN_REV_ID: 9d7401e5c629040fda7824966588af7f39e4a14c
This commit is contained in:
Daniel Harvey 2024-03-15 10:59:38 +00:00 committed by hasura-bot
parent a86d2d0450
commit c754f273eb
24 changed files with 706 additions and 46 deletions

View File

@ -13,7 +13,7 @@ use transitive::Transitive;
use crate::metadata::resolved::{ndc_validation::NDCValidationError, subgraph::Qualified};
use super::types::Annotation;
use super::types::{Annotation, NamespaceAnnotation};
#[derive(Error, Debug)]
pub enum InternalDeveloperError {
@ -88,6 +88,12 @@ pub enum InternalEngineError {
#[error("unexpected annotation: {annotation}")]
UnexpectedAnnotation { annotation: Annotation },
#[error("unexpected namespace annotation: {namespace_annotation:} found, expected type {expected_type:}")]
UnexpectedNamespaceAnnotation {
namespace_annotation: NamespaceAnnotation,
expected_type: String,
},
#[error("subscription shouldn't have been validated")]
SubscriptionsNotSupported,

View File

@ -1,15 +1,16 @@
//! IR and execution logic for commands
//!
//! A 'command' executes a function/procedure and returns back the result of the execution.
use hasura_authn_core::SessionVariables;
use lang_graphql::ast::common as ast;
use lang_graphql::ast::common::TypeContainer;
use lang_graphql::ast::common::TypeName;
use lang_graphql::normalized_ast;
use open_dds::arguments::ArgumentName;
use open_dds::commands;
use open_dds::commands::FunctionName;
use open_dds::commands::ProcedureName;
use open_dds::permissions::ValueExpression;
use serde::Serialize;
use serde_json as json;
use std::collections::BTreeMap;
@ -17,11 +18,13 @@ use std::collections::BTreeMap;
use super::arguments;
use super::selection_set;
use crate::execute::error;
use crate::execute::ir::permissions;
use crate::execute::model_tracking::{count_command, UsagesCounts};
use crate::metadata::resolved;
use crate::metadata::resolved::subgraph;
use crate::metadata::resolved::subgraph::QualifiedTypeReference;
use crate::schema::types::CommandSourceDetail;
use crate::schema::types::NamespaceAnnotation;
use crate::schema::types::TypeKind;
use crate::schema::GDS;
@ -86,6 +89,7 @@ pub(crate) fn generate_command_info<'n, 's>(
session_variables: &SessionVariables,
) -> Result<CommandInfo<'s>, error::Error> {
let mut command_arguments = BTreeMap::new();
for argument in field_call.arguments.values() {
command_arguments.extend(
arguments::build_ndc_command_arguments_as_value(
@ -97,6 +101,39 @@ pub(crate) fn generate_command_info<'n, 's>(
);
}
match field_call.info.namespaced {
None => {}
Some(NamespaceAnnotation::ArgumentPresets(preset_arguments)) => {
// add any preset arguments from command permissions
for (ArgumentName(argument_name_inner), (field_type, argument_value)) in
preset_arguments
{
let actual_value: serde_json::Value = match argument_value {
ValueExpression::Literal(val) => Ok(val.clone()),
ValueExpression::SessionVariable(session_var) => {
let value = session_variables.get(session_var).ok_or_else(|| {
error::InternalDeveloperError::MissingSessionVariable {
session_variable: session_var.clone(),
}
})?;
permissions::typecast_session_variable(value, field_type)
}
}?;
command_arguments.insert(argument_name_inner.to_string(), actual_value);
}
}
Some(other_annotation) => {
return Err(error::Error::InternalError(error::InternalError::Engine(
error::InternalEngineError::UnexpectedNamespaceAnnotation {
namespace_annotation: other_annotation.clone(),
expected_type: "ArgumentPresets".to_string(),
},
)))
}
}
// Add the name of the root command
let mut usage_counts = UsagesCounts::new();
count_command(command_name.clone(), &mut usage_counts);

View File

@ -32,6 +32,7 @@ pub(crate) fn get_select_filter_predicate<'s>(
.and_then(|annotation| match annotation {
types::NamespaceAnnotation::Filter(predicate) => Some(predicate),
types::NamespaceAnnotation::NodeFieldTypeMappings(_) => None,
types::NamespaceAnnotation::ArgumentPresets(_) => None,
})
// If we're hitting this case, it means that the caller of this
// function expects a filter predicate, but it was not annotated
@ -214,7 +215,7 @@ fn make_value_from_value_expression(
}
/// Typecast a stringified session variable into a given type, but as a serde_json::Value
fn typecast_session_variable(
pub(crate) fn typecast_session_variable(
session_var_value_wrapped: &SessionVariableValue,
to_type: &QualifiedTypeReference,
) -> Result<serde_json::Value, Error> {

View File

@ -15,7 +15,7 @@ use open_dds::commands::{
self, CommandName, CommandV1, DataConnectorCommand, GraphQlRootFieldKind,
};
use open_dds::data_connector::DataConnectorName;
use open_dds::permissions::{CommandPermissionsV1, Role};
use open_dds::permissions::{CommandPermissionsV1, Role, ValueExpression};
use open_dds::types::{BaseType, CustomTypeName, TypeName, TypeReference};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
@ -57,6 +57,7 @@ pub struct Command {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct CommandPermission {
pub allow_execution: bool,
pub argument_presets: BTreeMap<ArgumentName, (QualifiedTypeReference, ValueExpression)>,
}
fn is_valid_type(
@ -281,13 +282,44 @@ pub fn resolve_command_source(
}
pub fn resolve_command_permissions(
command: &Command,
permissions: &CommandPermissionsV1,
) -> Result<HashMap<Role, CommandPermission>, Error> {
let mut validated_permissions = HashMap::new();
for command_permission in &permissions.permissions {
// TODO: Use the permission predicates/presets
let mut argument_presets = BTreeMap::new();
for argument_preset in &command_permission.argument_presets {
if argument_presets.contains_key(&argument_preset.argument) {
return Err(Error::DuplicateCommandArgumentPreset {
command_name: command.name.clone(),
argument_name: argument_preset.argument.clone(),
});
}
// TODO: typecheck any literal values against the argument types
match command.arguments.get(&argument_preset.argument) {
Some(argument) => {
argument_presets.insert(
argument_preset.argument.clone(),
(
argument.argument_type.clone(),
argument_preset.value.clone(),
),
);
}
None => {
return Err(Error::CommandArgumentPresetMismatch {
command_name: command.name.clone(),
argument_name: argument_preset.argument.clone(),
});
}
}
}
let resolved_permission = CommandPermission {
allow_execution: command_permission.allow_execution,
argument_presets,
};
validated_permissions.insert(command_permission.role.clone(), resolved_permission);
}

View File

@ -120,6 +120,16 @@ pub enum Error {
command_name: Qualified<CommandName>,
argument_name: ArgumentName,
},
#[error("a preset argument {argument_name:} has been set for the command {command_name:} but no such argument exists for this command")]
CommandArgumentPresetMismatch {
command_name: Qualified<CommandName>,
argument_name: ArgumentName,
},
#[error("duplicate preset argument {argument_name:} for command {command_name:}")]
DuplicateCommandArgumentPreset {
command_name: Qualified<CommandName>,
argument_name: ArgumentName,
},
#[error("the procedure {procedure:} in the data connector {data_connector:} for command {command_name:} has not been defined")]
UnknownCommandProcedure {
command_name: Qualified<CommandName>,

View File

@ -510,7 +510,7 @@ pub fn resolve_metadata(metadata: open_dds::Metadata) -> Result<Metadata, Error>
}
})?;
if command.permissions.is_none() {
command.permissions = Some(resolve_command_permissions(command_permissions)?);
command.permissions = Some(resolve_command_permissions(command, command_permissions)?);
} else {
return Err(Error::DuplicateCommandPermission {
command_name: qualified_command_name.clone(),

View File

@ -165,7 +165,6 @@ pub enum ModelPredicate {
relationship_info: PredicateRelationshipAnnotation,
predicate: Box<ModelPredicate>,
},
And(Vec<ModelPredicate>),
Or(Vec<ModelPredicate>),
Not(Box<ModelPredicate>),

View File

@ -2,6 +2,10 @@
//!
//! A 'command' executes a function/procedure and returns back the result of the execution.
use crate::metadata::resolved;
use crate::schema::permissions;
use crate::schema::types::{self, output_type::get_output_type, Annotation};
use crate::schema::GDS;
use lang_graphql::ast::common as ast;
use lang_graphql::schema as gql_schema;
use lang_graphql::schema::InputField;
@ -12,11 +16,6 @@ use open_dds::commands::DataConnectorCommand;
use std::collections::HashMap;
use crate::metadata::resolved;
use crate::schema::permissions;
use crate::schema::types::{self, output_type::get_output_type, Annotation};
use crate::schema::GDS;
use super::types::output_type::get_type_kind;
pub enum Response {
@ -28,6 +27,7 @@ pub enum Response {
},
}
// look at the permissions and remove arguments with presets for this role
pub(crate) fn generate_command_argument(
gds: &GDS,
builder: &mut gql_schema::Builder<GDS>,
@ -37,29 +37,47 @@ pub(crate) fn generate_command_argument(
) -> Result<(ast::Name, Namespaced<GDS, InputField<GDS>>), crate::schema::Error> {
let field_name = ast::Name::new(argument_name.0.as_str())?;
let input_type = types::input_type::get_input_type(gds, builder, &argument_type.argument_type)?;
Ok((
let input_field = gql_schema::InputField::new(
field_name.clone(),
builder.allow_all_namespaced(
gql_schema::InputField::new(
argument_type.description.clone(),
Annotation::Input(types::InputAnnotation::CommandArgument {
argument_type: argument_type.argument_type.clone(),
ndc_func_proc_argument: command
.source
.as_ref()
.and_then(|command_source| command_source.argument_mappings.get(argument_name))
.cloned(),
}),
input_type,
None,
gql_schema::DeprecationStatus::NotDeprecated,
);
// a role is "allowed" to use this argument if it DOESN'T have a preset argument defined
match &command.permissions {
// if this command has any permissions, we must assume it has them setup for every role
// that is interested
Some(permissions_by_namespace) => {
let mut namespaced_annotations = HashMap::new();
for (namespace, permission) in permissions_by_namespace {
// if there is a preset for this argument, remove it from the schema
// so the user cannot provide one
if !permission.argument_presets.contains_key(argument_name) {
namespaced_annotations.insert(namespace.clone(), None);
}
}
Ok((
field_name,
argument_type.description.clone(),
Annotation::Input(types::InputAnnotation::CommandArgument {
argument_type: argument_type.argument_type.clone(),
ndc_func_proc_argument: command
.source
.as_ref()
.and_then(|command_source| {
command_source.argument_mappings.get(argument_name)
})
.cloned(),
}),
input_type,
None,
gql_schema::DeprecationStatus::NotDeprecated,
),
None,
),
))
builder.conditional_namespaced(input_field, namespaced_annotations),
))
}
// if there are no permissions for this command, there are no presets so we assume all
// arguments are OK to use
None => Ok((field_name, builder.allow_all_namespaced(input_field, None))),
}
}
pub(crate) fn command_field(
@ -78,6 +96,7 @@ pub(crate) fn command_field(
let output_typename = get_output_type(gds, builder, &command.output_type)?;
let mut arguments = HashMap::new();
for (argument_name, argument_type) in &command.arguments {
let (field_name, input_field) =
generate_command_argument(gds, builder, command, argument_name, argument_type)?;
@ -93,7 +112,7 @@ pub(crate) fn command_field(
arguments,
gql_schema::DeprecationStatus::NotDeprecated,
),
permissions::get_command_namespace_annotations(command),
permissions::get_command_namespace_annotations(command)?,
);
Ok((command_field_name, field))
}

View File

@ -79,19 +79,24 @@ pub(crate) fn get_model_relationship_namespace_annotations(
/// Build namespace annotation for commands
pub(crate) fn get_command_namespace_annotations(
command: &resolved::command::Command,
) -> HashMap<Role, Option<types::NamespaceAnnotation>> {
) -> Result<HashMap<Role, Option<types::NamespaceAnnotation>>, crate::schema::Error> {
let mut permissions = HashMap::new();
match &command.permissions {
Some(command_permissions) => {
for (role, permission) in command_permissions {
if permission.allow_execution {
permissions.insert(role.clone(), None);
permissions.insert(
role.clone(),
Some(types::NamespaceAnnotation::ArgumentPresets(
permission.argument_presets.clone(),
)),
);
}
}
}
None => {}
}
permissions
Ok(permissions)
}
/// Build namespace annotation for command relationship permissions.
@ -101,10 +106,10 @@ pub(crate) fn get_command_relationship_namespace_annotations(
command: &resolved::command::Command,
source_object_type_representation: &ObjectTypeRepresentation,
mappings: &[resolved::relationship::RelationshipCommandMapping],
) -> HashMap<Role, Option<types::NamespaceAnnotation>> {
let select_permissions = get_command_namespace_annotations(command);
) -> Result<HashMap<Role, Option<types::NamespaceAnnotation>>, crate::schema::Error> {
let select_permissions = get_command_namespace_annotations(command)?;
select_permissions
Ok(select_permissions
.into_iter()
.filter(|(role, _)| {
mappings.iter().all(|mapping| {
@ -115,7 +120,7 @@ pub(crate) fn get_command_relationship_namespace_annotations(
.any(|allowed_role| role == allowed_role)
})
})
.collect()
.collect())
}
/// Build namespace annotations for the node interface..

View File

@ -239,6 +239,16 @@ pub enum Annotation {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Display)]
pub enum NamespaceAnnotation {
Filter(resolved::model::FilterPermission),
/// any arguments that we should prefill for a command or type
ArgumentPresets(
BTreeMap<
ArgumentName,
(
QualifiedTypeReference,
open_dds::permissions::ValueExpression,
),
>,
),
/// The `NodeFieldTypeMappings` contains a Hashmap of typename to the filter permission.
/// While executing the `node` field, the `id` field is supposed to be decoded and after
/// decoding, a typename will be obtained. We need to use that typename to look up the

View File

@ -260,7 +260,7 @@ fn object_type_fields(
command,
object_type_representation,
mappings,
),
)?,
)
}
resolved::relationship::RelationshipTarget::Model {

View File

@ -120,6 +120,110 @@ pub fn test_execution_expectation_legacy(test_path_string: &str, common_metadata
});
}
#[allow(dead_code)]
pub(crate) fn test_introspection_expectation(
test_path_string: &str,
common_metadata_paths: &[&str],
) {
tokio_test::block_on(async {
// Setup test context
let root_test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests");
let mut test_ctx = setup(&root_test_dir);
let test_path = root_test_dir.join(test_path_string);
let request_path = test_path.join("introspection_request.gql");
let response_path = test_path_string.to_string() + "/introspection_expected.json";
let metadata_path = test_path.join("metadata.json");
let metadata_json_value = merge_with_common_metadata(
&metadata_path,
common_metadata_paths
.iter()
.map(|path| root_test_dir.join(path)),
);
let metadata = open_dds::traits::OpenDd::deserialize(metadata_json_value).unwrap();
// TODO: remove this assert once we have stopped manually implementing Serialize for OpenDD types.
assert_eq!(
open_dds::Metadata::from_json_str(&serde_json::to_string(&metadata).unwrap()).unwrap(),
metadata
);
let gds = GDS::new(metadata).unwrap();
let schema = GDS::build_schema(&gds).unwrap();
// Verify successful serialization and deserialization of the schema.
// Hasura V3 relies on the serialized schema for handling requests.
// Therefore, it is crucial to ensure the functionality of both
// deserialization and serialization.
// Testing this within this function allows us to detect errors for any
// future metadata tests that may be added.
let serialized_metadata =
serde_json::to_string(&schema).expect("Failed to serialize schema");
let deserialized_metadata: Schema<GDS> =
serde_json::from_str(&serialized_metadata).expect("Failed to deserialize metadata");
assert_eq!(
schema, deserialized_metadata,
"initial built metadata does not match deserialized metadata"
);
let query = fs::read_to_string(request_path).unwrap();
let session_vars_path = &test_path.join("session_variables.json");
let sessions: Vec<HashMap<SessionVariable, SessionVariableValue>> =
json::from_str(fs::read_to_string(session_vars_path).unwrap().as_ref()).unwrap();
let sessions: Vec<Session> = sessions.into_iter().map(resolve_session).collect();
assert!(
sessions.len() > 1,
"Found less than 2 roles in test scenario"
);
let raw_request = RawRequest {
operation_name: None,
query,
variables: None,
};
// Execute the test
let mut responses = Vec::new();
for session in sessions.iter() {
let response = execute_query(
&test_ctx.http_client,
&schema,
session,
raw_request.clone(),
None,
)
.await;
responses.push(response.0);
}
let mut expected = test_ctx
.mint
.new_goldenfile_with_differ(
response_path,
Box::new(|file1, file2| {
let json1: serde_json::Value =
serde_json::from_reader(File::open(file1).unwrap()).unwrap();
let json2: serde_json::Value =
serde_json::from_reader(File::open(file2).unwrap()).unwrap();
if json1 != json2 {
text_diff(file1, file2)
}
}),
)
.unwrap();
write!(
expected,
"{}",
serde_json::to_string_pretty(&responses).unwrap()
)
.unwrap();
});
}
pub fn test_execution_expectation(test_path_string: &str, common_metadata_paths: &[&str]) {
tokio_test::block_on(async {
// Setup test context

View File

@ -0,0 +1,47 @@
[
{
"data": {
"getActorsByMovieIdBounds": [
{
"actor_id": 3,
"movie_id": 2,
"name": "Irfan Khan"
},
{
"actor_id": 4,
"movie_id": 3,
"name": "Al Pacino"
},
{
"actor_id": 5,
"movie_id": 3,
"name": "Robert De Niro"
},
{
"actor_id": 6,
"movie_id": 4,
"name": "Morgan Freeman"
}
]
}
},
{
"data": {
"getActorsByMovieIdBounds": [
{
"actor_id": 6,
"movie_id": 4,
"name": "Morgan Freeman"
}
]
}
},
{
"data": null,
"errors": [
{
"message": "validation failed: required argument lower_bound not found on field getActorsByMovieIdBounds of type Query"
}
]
}
]

View File

@ -0,0 +1,98 @@
[
{
"data": {
"__schema": {
"queryType": {
"name": "Query",
"fields": [
{
"name": "getActorsByMovieIdBounds",
"args": [
{
"name": "upper_bound",
"defaultValue": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int"
}
}
}
]
}
]
}
}
}
},
{
"data": {
"__schema": {
"queryType": {
"name": "Query",
"fields": [
{
"name": "getActorsByMovieIdBounds",
"args": [
{
"name": "upper_bound",
"defaultValue": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int"
}
}
}
]
}
]
}
}
}
},
{
"data": {
"__schema": {
"queryType": {
"name": "Query",
"fields": [
{
"name": "getActorsByMovieIdBounds",
"args": [
{
"name": "lower_bound",
"defaultValue": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int"
}
}
},
{
"name": "upper_bound",
"defaultValue": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int"
}
}
}
]
}
]
}
}
}
}
]

View File

@ -0,0 +1,22 @@
query {
__schema {
queryType {
name
fields {
name
args {
name
defaultValue
type {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}

View File

@ -0,0 +1,118 @@
{
"version": "v2",
"subgraphs": [
{
"name": "default",
"objects": [
{
"kind": "TypePermissions",
"version": "v1",
"definition": {
"typeName": "commandActor",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": [
"actor_id",
"name",
"movie_id"
]
}
},
{
"role": "user_1",
"output": {
"allowedFields": [
"actor_id",
"name",
"movie_id"
]
}
},
{
"role": "user_2",
"output": {
"allowedFields": [
"actor_id",
"name",
"movie_id"
]
}
}
]
}
},
{
"kind": "CommandPermissions",
"version": "v1",
"definition": {
"commandName": "get_actors_by_movie_id_bounds",
"permissions": [
{
"role": "admin",
"allowExecution": true,
"argumentPresets": [
{
"argument": "lower_bound",
"value": {
"literal": 2
}
}
]
},
{
"role": "user_1",
"allowExecution": true,
"argumentPresets": [
{
"argument": "lower_bound",
"value": {
"literal": 4
}
}
]
},
{
"role": "user_2",
"allowExecution": true
}
]
}
},
{
"kind": "Command",
"version": "v1",
"definition": {
"name": "get_actors_by_movie_id_bounds",
"arguments": [
{
"name": "lower_bound",
"type": "Int!"
},
{
"name": "upper_bound",
"type": "Int!"
}
],
"outputType": "[commandActor]",
"source": {
"dataConnectorName": "custom",
"dataConnectorCommand": {
"function": "get_actors_by_movie_id_bounds"
},
"argumentMapping": {
"lower_bound": "lower_bound",
"upper_bound": "upper_bound"
}
},
"graphql": {
"rootFieldName": "getActorsByMovieIdBounds",
"rootFieldKind": "Query"
}
}
}
]
}
]
}

View File

@ -0,0 +1,7 @@
query MyQuery {
getActorsByMovieIdBounds(upper_bound: 4) {
actor_id
movie_id
name
}
}

View File

@ -0,0 +1,11 @@
[
{
"x-hasura-role": "admin"
},
{
"x-hasura-role": "user_1"
},
{
"x-hasura-role": "user_2"
}
]

View File

@ -735,6 +735,24 @@ fn test_command_procedures_multiple_arguments() {
);
}
// Tests a mutation command with preset arguments:
// arguments: 1 arguments (taken as id and new name for an actor and returns the updated commandActor row )
// output: object (commandActor) output type
// permission: different permissions and preset arguments for roles: admin, user_1, user_2
#[test]
fn test_command_functions_preset_arguments() {
let test_path_string = "execute/commands/functions/preset_arguments";
let common_metadata_path_string = "execute/common_metadata/custom_connector_schema.json";
let common_command_metadata_path_string = "execute/common_metadata/command_metadata.json";
common::test_execution_expectation(
test_path_string,
&[
common_metadata_path_string,
common_command_metadata_path_string,
],
);
}
// Tests using relationships in predicates
// Array relationship
#[test]

View File

@ -0,0 +1,16 @@
#[allow(dead_code)]
mod common;
#[test]
fn test_introspect_command_with_preset_arguments() {
let common_metadata_path_string = "execute/common_metadata/custom_connector_schema.json";
let common_command_metadata_path_string = "execute/common_metadata/command_metadata.json";
common::test_introspection_expectation(
"execute/commands/functions/preset_arguments/",
&[
common_metadata_path_string,
common_command_metadata_path_string,
],
);
}

View File

@ -158,6 +158,7 @@ where
let mut field_calls = HashMap::new();
for (reachability, fields) in typed_fields.into_iter() {
let cannonical_field = fields.head;
let arguments = normalize_arguments(
namespace,
schema,

View File

@ -2686,7 +2686,15 @@
"examples": [
{
"role": "user",
"allowExecution": true
"allowExecution": true,
"argumentPresets": [
{
"argument": "user_id",
"value": {
"session_variable": "x-hasura-user_id"
}
}
]
}
],
"type": "object",
@ -2706,10 +2714,78 @@
"allowExecution": {
"description": "Whether the command is executable by the role.",
"type": "boolean"
},
"argumentPresets": {
"description": "Preset values for arguments for this role",
"default": [],
"type": "array",
"items": {
"$ref": "#/definitions/ArgumentPreset"
}
}
},
"additionalProperties": false
},
"ArgumentPreset": {
"$id": "https://hasura.io/jsonschemas/metadata/ArgumentPreset",
"title": "ArgumentPreset",
"description": "Preset value for an argument",
"type": "object",
"required": [
"argument",
"value"
],
"properties": {
"argument": {
"description": "Argument name for preset",
"allOf": [
{
"$ref": "#/definitions/ArgumentName"
}
]
},
"value": {
"description": "Value for preset",
"allOf": [
{
"$ref": "#/definitions/ValueExpression2"
}
]
}
},
"additionalProperties": false
},
"ValueExpression2": {
"$id": "https://hasura.io/jsonschemas/metadata/ValueExpression2",
"title": "ValueExpression",
"description": "An expression which evaluates to a value that can be used in permissions.",
"oneOf": [
{
"title": "Literal",
"type": "object",
"required": [
"literal"
],
"properties": {
"literal": true
},
"additionalProperties": false
},
{
"title": "SessionVariable",
"type": "object",
"required": [
"sessionVariable"
],
"properties": {
"sessionVariable": {
"$ref": "#/definitions/OpenDdSessionVariable"
}
},
"additionalProperties": false
}
]
},
"OpenDdMetadataWithVersion": {
"$id": "https://hasura.io/jsonschemas/metadata/OpenDdMetadataWithVersion",
"title": "OpenDdMetadataWithVersion",

View File

@ -9,6 +9,8 @@ use crate::{impl_JsonSchema_with_OpenDd_for, types::TypeReference};
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
derive_more::Display,
opendds_derive::OpenDd,

View File

@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use crate::{
arguments::ArgumentName,
commands::CommandName,
models::ModelName,
relationships::RelationshipName,
@ -33,6 +34,16 @@ impl Role {
}
}
#[derive(Serialize, Clone, Debug, Eq, PartialEq, opendds_derive::OpenDd)]
#[serde(rename_all = "camelCase")]
/// Preset value for an argument
pub struct ArgumentPreset {
/// Argument name for preset
pub argument: ArgumentName,
/// Value for preset
pub value: ValueExpression,
}
#[derive(Serialize, Clone, Debug, Eq, PartialEq, opendds_derive::OpenDd)]
#[serde(tag = "version", content = "definition")]
#[serde(rename_all = "camelCase")]
@ -293,9 +304,11 @@ impl NullableModelPredicate {
pub struct CommandPermission {
/// The role for which permissions are being defined.
pub role: Role,
// TODO: Implement predicates and presets
/// Whether the command is executable by the role.
pub allow_execution: bool,
/// Preset values for arguments for this role
#[opendd(default, json_schema(default_exp = "serde_json::json!([])"))]
pub argument_presets: Vec<ArgumentPreset>,
}
impl CommandPermission {
@ -303,7 +316,13 @@ impl CommandPermission {
serde_json::json!(
{
"role": "user",
"allowExecution": true
"allowExecution": true,
"argumentPresets": [{
"argument": "user_id",
"value": {
"session_variable": "x-hasura-user_id"
}
}]
}
)
}
@ -439,7 +458,9 @@ impl ModelPredicate {
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
#[derive(
Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, opendds_derive::OpenDd,
)]
#[serde(rename_all = "camelCase")]
#[schemars(title = "ValueExpression")]
/// An expression which evaluates to a value that can be used in permissions.