diff --git a/v3/gds/src/metadata/resolved.rs b/v3/gds/src/metadata/resolved.rs index 66ec448a58070e..1b92f0af0a9ea4 100644 --- a/v3/gds/src/metadata/resolved.rs +++ b/v3/gds/src/metadata/resolved.rs @@ -245,6 +245,10 @@ pub enum Error { model_name: ModelName, operator_name: OperatorName, }, + #[error("unknown command used in command permissions definition: {command_name:}")] + UnknownCommandInCommandPermissions { command_name: CommandName }, + #[error("multiple permissions defined for command: {command_name:}")] + DuplicateCommandPermission { command_name: CommandName }, #[error("{message:}")] UnsupportedFeature { message: String }, @@ -582,6 +586,7 @@ pub struct Command { pub arguments: IndexMap, pub graphql_api: Option, pub source: Option, + pub permissions: Option>, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] @@ -592,6 +597,11 @@ pub struct CommandSource { pub argument_mappings: HashMap, } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct CommandPermission { + pub allow_execution: bool, +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct SelectPermission { // Missing filter implies all rows are selectable @@ -827,6 +837,22 @@ pub fn resolve_metadata(metadata: &open_dds::Metadata) -> Result Result, Error> { + let mut validated_permissions = HashMap::new(); + for (role, permission) in &permissions.permissions { + // TODO: Use the permission predicates/presets + let resolved_permission = CommandPermission { + allow_execution: permission.allow_execution, + }; + validated_permissions.insert(role.clone(), resolved_permission); + } + Ok(validated_permissions) +} + fn resolve_model_select_permissions( model: &Model, select_permissions: &open_dds::data_specification::ModelSelectPermissions, diff --git a/v3/gds/src/schema/operations/commands.rs b/v3/gds/src/schema/operations/commands.rs index a5a31f2e01bcaa..1553b031d256f6 100644 --- a/v3/gds/src/schema/operations/commands.rs +++ b/v3/gds/src/schema/operations/commands.rs @@ -23,6 +23,7 @@ use tracing_util::FutureTracing; use super::response_processing::process_command_rows; use super::Error; use crate::metadata::resolved; +use crate::schema::operations::permissions; use crate::schema::types::command_arguments; use crate::schema::types::{self, output_type::get_output_type, Annotation}; use crate::schema::GDS; @@ -104,7 +105,7 @@ pub(crate) fn command_field( arguments.insert(field_name, input_field); } - let field = gql_schema::Namespaced::new_allow_all( + let field = gql_schema::Namespaced::new_conditional( gql_schema::Field::new( command_field_name.clone(), None, @@ -117,7 +118,7 @@ pub(crate) fn command_field( arguments, gql_schema::DeprecationStatus::NotDeprecated, ), - None, + permissions::get_command_namespace_annotations(command), ); Ok((command_field_name, field)) } diff --git a/v3/gds/src/schema/operations/permissions.rs b/v3/gds/src/schema/operations/permissions.rs index ad074ab6e2c436..8439f587dc038c 100644 --- a/v3/gds/src/schema/operations/permissions.rs +++ b/v3/gds/src/schema/operations/permissions.rs @@ -220,3 +220,21 @@ pub(crate) fn get_select_namespace_annotations( }) .unwrap_or_else(HashMap::new) } + +/// Build namespace annotation for commands +pub(crate) fn get_command_namespace_annotations( + command: &resolved::Command, +) -> HashMap> { + 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); + } + } + } + None => {} + } + permissions +} diff --git a/v3/gds/tests/execute/introspection_user_1/expected.json b/v3/gds/tests/execute/introspection_user_1/expected.json index 9bda78eb62907c..b71efb4b48a1ec 100644 --- a/v3/gds/tests/execute/introspection_user_1/expected.json +++ b/v3/gds/tests/execute/introspection_user_1/expected.json @@ -257,29 +257,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "CommandArticle", - "description": null, - "fields": [ - { - "name": "_no_fields_accessible", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, { "kind": "SCALAR", "name": "Float", @@ -316,46 +293,13 @@ "description": null, "fields": [ { - "name": "updateArticleTitleById", + "name": "_no_fields_accessible", "description": null, - "args": [ - { - "name": "article_id", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "title", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null - } - ], + "args": [], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "CommandArticle", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "isDeprecated": false, "deprecationReason": null @@ -495,69 +439,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "getArticleById", - "description": null, - "args": [ - { - "name": "article_id", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "CommandArticle", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "getLatestArticle", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "CommandArticle", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "getLatestArticleId", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "node", "description": null, diff --git a/v3/gds/tests/schema.json b/v3/gds/tests/schema.json index 8065a168423f9b..b7d0b7d66062ed 100644 --- a/v3/gds/tests/schema.json +++ b/v3/gds/tests/schema.json @@ -895,6 +895,25 @@ "title", "author_id" ] + }, + "user": { + "fields": [ + "article_id", + "title", + "author_id" + ] + } + } + }, + { + "kind": "CommandPermissions", + "commandName": "get_article_by_id", + "permissions": { + "admin": { + "allowExecution": true + }, + "user": { + "allowExecution": true } } }, @@ -935,6 +954,15 @@ "rootFieldKind": "Query" } }, + { + "kind": "CommandPermissions", + "commandName": "get_latest_article", + "permissions": { + "admin": { + "allowExecution": true + } + } + }, { "kind": "Command", "name": "get_latest_article", @@ -964,6 +992,15 @@ "rootFieldKind": "Query" } }, + { + "kind": "CommandPermissions", + "commandName": "get_latest_article_id", + "permissions": { + "admin": { + "allowExecution": true + } + } + }, { "kind": "Command", "name": "get_latest_article_id", @@ -978,6 +1015,15 @@ "rootFieldKind": "Query" } }, + { + "kind": "CommandPermissions", + "commandName": "update_article_title_by_id", + "permissions": { + "admin": { + "allowExecution": true + } + } + }, { "kind": "Command", "name": "update_article_title_by_id", diff --git a/v3/open-dds/open_dds_object.jsonschema b/v3/open-dds/open_dds_object.jsonschema index fd439175395071..193e1c69e81488 100644 --- a/v3/open-dds/open_dds_object.jsonschema +++ b/v3/open-dds/open_dds_object.jsonschema @@ -362,6 +362,33 @@ } } } + }, + { + "title": "CommandPermissions", + "description": "Role-Permission map for a command", + "type": "object", + "required": [ + "commandName", + "kind", + "permissions" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "CommandPermissions" + ] + }, + "commandName": { + "$ref": "#/definitions/CommandName" + }, + "permissions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/CommandPermission" + } + } + } } ], "definitions": { @@ -1152,6 +1179,18 @@ }, "OperatorName": { "type": "string" + }, + "CommandPermission": { + "title": "CommandPermission", + "type": "object", + "required": [ + "allowExecution" + ], + "properties": { + "allowExecution": { + "type": "boolean" + } + } } } } \ No newline at end of file diff --git a/v3/open-dds/src/accessor.rs b/v3/open-dds/src/accessor.rs index 91e9a384fba9cc..feb028b31c6ca9 100644 --- a/v3/open-dds/src/accessor.rs +++ b/v3/open-dds/src/accessor.rs @@ -14,6 +14,7 @@ pub struct MetadataAccessor<'a> { pub select_permissions: Vec<&'a data_specification::ModelSelectPermissions>, pub relationships: Vec<&'a data_specification::Relationship>, pub commands: Vec<&'a data_specification::Command>, + pub command_permissions: Vec<&'a data_specification::CommandPermissions>, } fn load_metadata_objects<'a>( @@ -51,6 +52,9 @@ fn load_metadata_objects<'a>( OpenDdsObject::Command(command) => { accessor.commands.push(command); } + OpenDdsObject::CommandPermissions(permissions_map) => { + accessor.command_permissions.push(permissions_map); + } } } } diff --git a/v3/open-dds/src/data_specification.rs b/v3/open-dds/src/data_specification.rs index f7c90c14937f9e..de13b133ec56fa 100644 --- a/v3/open-dds/src/data_specification.rs +++ b/v3/open-dds/src/data_specification.rs @@ -351,6 +351,23 @@ pub struct ModelSelectPermissions { pub permissions: HashMap, } +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] +#[serde(rename_all = "camelCase")] +#[schemars(title = "CommandPermission")] +pub struct CommandPermission { + // TODO: Implement predicates and presets + pub allow_execution: bool, +} + +/// Role-Permission map for a command +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] +#[serde(rename_all = "camelCase")] +#[schemars(title = "CommandPermissions")] +pub struct CommandPermissions { + pub command_name: CommandName, + pub permissions: HashMap, +} + #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] #[serde(rename_all = "camelCase")] #[schemars(title = "SelectPermission")] diff --git a/v3/open-dds/src/lib.rs b/v3/open-dds/src/lib.rs index 67a84a1530d3b2..4a999bd640a29f 100644 --- a/v3/open-dds/src/lib.rs +++ b/v3/open-dds/src/lib.rs @@ -32,6 +32,7 @@ pub enum OpenDdsObject { // Permissions TypeOutputPermissions(dds::TypeOutputPermissions), ModelSelectPermissions(dds::ModelSelectPermissions), + CommandPermissions(dds::CommandPermissions), // Runtime configuration // TODO }