From b241cf7a44d894c6e703ec1004aa108b8cc11a89 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Mon, 11 Sep 2023 13:12:03 -0700 Subject: [PATCH] Add input validation for `user_role_permission` table --- .../down.sql | 5 + .../28_validate_user_role_permissions/up.sql | 145 ++++++++++++++++++ .../sql/merlin/applied_migrations.sql | 1 + .../sql/merlin/domain-types/permissions.sql | 2 + .../tables/metadata/user_role_permission.sql | 143 +++++++++++++++++ 5 files changed, 296 insertions(+) create mode 100644 deployment/hasura/migrations/AerieMerlin/28_validate_user_role_permissions/down.sql create mode 100644 deployment/hasura/migrations/AerieMerlin/28_validate_user_role_permissions/up.sql diff --git a/deployment/hasura/migrations/AerieMerlin/28_validate_user_role_permissions/down.sql b/deployment/hasura/migrations/AerieMerlin/28_validate_user_role_permissions/down.sql new file mode 100644 index 0000000000..5a254686bc --- /dev/null +++ b/deployment/hasura/migrations/AerieMerlin/28_validate_user_role_permissions/down.sql @@ -0,0 +1,5 @@ +drop trigger validate_permissions_trigger + on metadata.user_role_permission; +drop function metadata.validate_permissions_json(); + +call migrations.mark_migration_rolled_back('28'); diff --git a/deployment/hasura/migrations/AerieMerlin/28_validate_user_role_permissions/up.sql b/deployment/hasura/migrations/AerieMerlin/28_validate_user_role_permissions/up.sql new file mode 100644 index 0000000000..228c0eb3c5 --- /dev/null +++ b/deployment/hasura/migrations/AerieMerlin/28_validate_user_role_permissions/up.sql @@ -0,0 +1,145 @@ +create function metadata.validate_permissions_json() +returns trigger +language plpgsql as $$ + declare + error_msg text; + plan_merge_fns text[]; +begin + error_msg = ''; + + plan_merge_fns := '{ + "begin_merge", + "cancel_merge", + "commit_merge", + "create_merge_rq", + "deny_merge", + "get_conflicting_activities", + "get_non_conflicting_activities", + "set_resolution", + "set_resolution_bulk", + "withdraw_merge_rq" + }'; + + -- Do all the validation checks up front + -- Duplicate keys are not checked for, as as all but the last instance is removed + -- during conversion of JSON Text to JSONB (https://www.postgresql.org/docs/14/datatype-json.html) + create temp table _validate_functions_table as + select + jsonb_object_keys(new.function_permissions) as function_key, + new.function_permissions ->> jsonb_object_keys(new.function_permissions) as function_permission, + jsonb_object_keys(new.function_permissions) = any(enum_range(null::metadata.function_permission_key)::text[]) as valid_function_key, + new.function_permissions ->> jsonb_object_keys(new.function_permissions) = any(enum_range(null::metadata.permission)::text[]) as valid_function_permission, + jsonb_object_keys(new.function_permissions) = any(plan_merge_fns) as is_plan_merge_key, + new.function_permissions ->> jsonb_object_keys(new.function_permissions) = any(enum_range('PLAN_OWNER_SOURCE'::metadata.permission, 'PLAN_OWNER_COLLABORATOR_TARGET'::metadata.permission)::text[]) as is_plan_merge_permission; + + create temp table _validate_actions_table as + select + jsonb_object_keys(new.action_permissions) as action_key, + new.action_permissions ->> jsonb_object_keys(new.action_permissions) as action_permission, + jsonb_object_keys(new.action_permissions) = any(enum_range(null::metadata.action_permission_key)::text[]) as valid_action_key, + new.action_permissions ->> jsonb_object_keys(new.action_permissions) = any(enum_range(null::metadata.permission)::text[]) as valid_action_permission, + new.action_permissions ->> jsonb_object_keys(new.action_permissions) = any(enum_range('PLAN_OWNER_SOURCE'::metadata.permission, 'PLAN_OWNER_COLLABORATOR_TARGET'::metadata.permission)::text[]) as is_plan_merge_permission; + + + -- Get any invalid Action Keys + if exists(select from _validate_actions_table where not valid_action_key) + then + error_msg = 'The following action keys are not valid: ' + || (select string_agg(action_key, ', ') + from _validate_actions_table + where not valid_action_key) + ||e'\n'; + end if; + -- Get any invalid Function Keys + if exists(select from _validate_functions_table where not valid_function_key) + then + error_msg = error_msg + || 'The following function keys are not valid: ' + || (select string_agg(function_key, ', ') + from _validate_functions_table + where not valid_function_key); + end if; + + -- Raise if there were invalid Action/Function Keys + if error_msg != '' then + raise exception using + message = 'invalid keys in supplied row', + detail = trim(both e'\n' from error_msg), + errcode = 'invalid_json_text', + hint = 'Visit https://nasa-ammos.github.io/aerie-docs/deployment/advanced-permissions/#action-and-function-permissions for a list of valid keys.'; + end if; + + -- Get any values that aren't Action Permissions + if exists(select from _validate_actions_table where not valid_action_permission) + then + error_msg = 'The following action keys have invalid permissions: {' + || (select string_agg(action_key || ': ' || action_permission, ', ') + from _validate_actions_table + where not valid_action_permission) + ||e'}\n'; + end if; + + -- Get any values that aren't Function Permissions + if exists(select from _validate_functions_table where not valid_function_permission) + then + error_msg = error_msg + || 'The following function keys have invalid permissions: {' + || (select string_agg(function_key || ': ' || function_permission, ', ') + from _validate_functions_table + where not valid_function_permission) + || '}'; + end if; + + -- Raise if there were invalid Action/Function Permissions + if error_msg != '' then + raise exception using + message = 'invalid permissions in supplied row', + detail = trim(both e'\n' from error_msg), + errcode = 'invalid_json_text', + hint = 'Visit https://nasa-ammos.github.io/aerie-docs/deployment/advanced-permissions/#action-and-function-permissions for a list of valid Permissions.'; + end if; + + -- Check that no Actions have Plan Merge Permissions + if exists(select from _validate_actions_table where is_plan_merge_permission) + then + error_msg = 'The following action keys may not take plan merge permissions: {' + || (select string_agg(action_key || ': ' || action_permission, ', ') + from _validate_actions_table + where is_plan_merge_permission) + ||e'}\n'; + end if; + + -- Check that no non-Plan Merge Functions have Plan Merge Permissions + if exists(select from _validate_functions_table where is_plan_merge_permission and not is_plan_merge_key) + then + error_msg = error_msg + || 'The following function keys may not take plan merge permissions: {' + || (select string_agg(function_key || ': ' || function_permission, ', ') + from _validate_functions_table + where is_plan_merge_permission and not is_plan_merge_key) + || '}'; + end if; + + -- Raise if Plan Merge Permissions were improperly applied + if error_msg != '' then + raise exception using + message = 'invalid permissions in supplied row', + detail = trim(both e'\n' from error_msg), + errcode = 'invalid_json_text', + hint = 'Visit https://nasa-ammos.github.io/aerie-docs/deployment/advanced-permissions/#action-and-function-permissions for more information.'; + end if; + + -- Drop Temp Tables + drop table _validate_functions_table; + drop table _validate_actions_table; + + return new; +end +$$; + +create trigger validate_permissions_trigger + before insert or update on metadata.user_role_permission + for each row + execute function metadata.validate_permissions_json(); + +call migrations.mark_migration_applied('28'); diff --git a/merlin-server/sql/merlin/applied_migrations.sql b/merlin-server/sql/merlin/applied_migrations.sql index 63c2754eaa..b71dbc02ea 100644 --- a/merlin-server/sql/merlin/applied_migrations.sql +++ b/merlin-server/sql/merlin/applied_migrations.sql @@ -30,3 +30,4 @@ call migrations.mark_migration_applied('24'); call migrations.mark_migration_applied('25'); call migrations.mark_migration_applied('26'); call migrations.mark_migration_applied('27'); +call migrations.mark_migration_applied('28'); diff --git a/merlin-server/sql/merlin/domain-types/permissions.sql b/merlin-server/sql/merlin/domain-types/permissions.sql index 630d89b2d5..9a6fd62d40 100644 --- a/merlin-server/sql/merlin/domain-types/permissions.sql +++ b/merlin-server/sql/merlin/domain-types/permissions.sql @@ -1,3 +1,5 @@ +-- User Role Permissions Validation assumes that the Plan Merge Permissions +-- are covered by the range [PLAN_OWNER_SOURCE - PLAN_OWNER_COLLABORATOR_TARGET] create type metadata.permission as enum ( 'NO_CHECK', diff --git a/merlin-server/sql/merlin/tables/metadata/user_role_permission.sql b/merlin-server/sql/merlin/tables/metadata/user_role_permission.sql index 9a634b0cc5..46f66d78bc 100644 --- a/merlin-server/sql/merlin/tables/metadata/user_role_permission.sql +++ b/merlin-server/sql/merlin/tables/metadata/user_role_permission.sql @@ -18,3 +18,146 @@ comment on column metadata.user_role_permission.action_permissions is '' comment on column metadata.user_role_permission.function_permissions is '' 'The permissions the role has on Hasura Functions.'; +create function metadata.validate_permissions_json() +returns trigger +language plpgsql as $$ + declare + error_msg text; + plan_merge_fns text[]; +begin + error_msg = ''; + + plan_merge_fns := '{ + "begin_merge", + "cancel_merge", + "commit_merge", + "create_merge_rq", + "deny_merge", + "get_conflicting_activities", + "get_non_conflicting_activities", + "set_resolution", + "set_resolution_bulk", + "withdraw_merge_rq" + }'; + + -- Do all the validation checks up front + -- Duplicate keys are not checked for, as as all but the last instance is removed + -- during conversion of JSON Text to JSONB (https://www.postgresql.org/docs/14/datatype-json.html) + create temp table _validate_functions_table as + select + jsonb_object_keys(new.function_permissions) as function_key, + new.function_permissions ->> jsonb_object_keys(new.function_permissions) as function_permission, + jsonb_object_keys(new.function_permissions) = any(enum_range(null::metadata.function_permission_key)::text[]) as valid_function_key, + new.function_permissions ->> jsonb_object_keys(new.function_permissions) = any(enum_range(null::metadata.permission)::text[]) as valid_function_permission, + jsonb_object_keys(new.function_permissions) = any(plan_merge_fns) as is_plan_merge_key, + new.function_permissions ->> jsonb_object_keys(new.function_permissions) = any(enum_range('PLAN_OWNER_SOURCE'::metadata.permission, 'PLAN_OWNER_COLLABORATOR_TARGET'::metadata.permission)::text[]) as is_plan_merge_permission; + + create temp table _validate_actions_table as + select + jsonb_object_keys(new.action_permissions) as action_key, + new.action_permissions ->> jsonb_object_keys(new.action_permissions) as action_permission, + jsonb_object_keys(new.action_permissions) = any(enum_range(null::metadata.action_permission_key)::text[]) as valid_action_key, + new.action_permissions ->> jsonb_object_keys(new.action_permissions) = any(enum_range(null::metadata.permission)::text[]) as valid_action_permission, + new.action_permissions ->> jsonb_object_keys(new.action_permissions) = any(enum_range('PLAN_OWNER_SOURCE'::metadata.permission, 'PLAN_OWNER_COLLABORATOR_TARGET'::metadata.permission)::text[]) as is_plan_merge_permission; + + + -- Get any invalid Action Keys + if exists(select from _validate_actions_table where not valid_action_key) + then + error_msg = 'The following action keys are not valid: ' + || (select string_agg(action_key, ', ') + from _validate_actions_table + where not valid_action_key) + ||e'\n'; + end if; + -- Get any invalid Function Keys + if exists(select from _validate_functions_table where not valid_function_key) + then + error_msg = error_msg + || 'The following function keys are not valid: ' + || (select string_agg(function_key, ', ') + from _validate_functions_table + where not valid_function_key); + end if; + + -- Raise if there were invalid Action/Function Keys + if error_msg != '' then + raise exception using + message = 'invalid keys in supplied row', + detail = trim(both e'\n' from error_msg), + errcode = 'invalid_json_text', + hint = 'Visit https://nasa-ammos.github.io/aerie-docs/deployment/advanced-permissions/#action-and-function-permissions for a list of valid keys.'; + end if; + + -- Get any values that aren't Action Permissions + if exists(select from _validate_actions_table where not valid_action_permission) + then + error_msg = 'The following action keys have invalid permissions: {' + || (select string_agg(action_key || ': ' || action_permission, ', ') + from _validate_actions_table + where not valid_action_permission) + ||e'}\n'; + end if; + + -- Get any values that aren't Function Permissions + if exists(select from _validate_functions_table where not valid_function_permission) + then + error_msg = error_msg + || 'The following function keys have invalid permissions: {' + || (select string_agg(function_key || ': ' || function_permission, ', ') + from _validate_functions_table + where not valid_function_permission) + || '}'; + end if; + + -- Raise if there were invalid Action/Function Permissions + if error_msg != '' then + raise exception using + message = 'invalid permissions in supplied row', + detail = trim(both e'\n' from error_msg), + errcode = 'invalid_json_text', + hint = 'Visit https://nasa-ammos.github.io/aerie-docs/deployment/advanced-permissions/#action-and-function-permissions for a list of valid Permissions.'; + end if; + + -- Check that no Actions have Plan Merge Permissions + if exists(select from _validate_actions_table where is_plan_merge_permission) + then + error_msg = 'The following action keys may not take plan merge permissions: {' + || (select string_agg(action_key || ': ' || action_permission, ', ') + from _validate_actions_table + where is_plan_merge_permission) + ||e'}\n'; + end if; + + -- Check that no non-Plan Merge Functions have Plan Merge Permissions + if exists(select from _validate_functions_table where is_plan_merge_permission and not is_plan_merge_key) + then + error_msg = error_msg + || 'The following function keys may not take plan merge permissions: {' + || (select string_agg(function_key || ': ' || function_permission, ', ') + from _validate_functions_table + where is_plan_merge_permission and not is_plan_merge_key) + || '}'; + end if; + + -- Raise if Plan Merge Permissions were improperly applied + if error_msg != '' then + raise exception using + message = 'invalid permissions in supplied row', + detail = trim(both e'\n' from error_msg), + errcode = 'invalid_json_text', + hint = 'Visit https://nasa-ammos.github.io/aerie-docs/deployment/advanced-permissions/#action-and-function-permissions for more information.'; + end if; + + -- Drop Temp Tables + drop table _validate_functions_table; + drop table _validate_actions_table; + + return new; +end +$$; + +create trigger validate_permissions_trigger + before insert or update on metadata.user_role_permission + for each row + execute function metadata.validate_permissions_json();