From 967718da793b30bec39b4b1a9fb6d0c979c4d3f9 Mon Sep 17 00:00:00 2001 From: Georgy Moshkin <gmoshkin@picodata.io> Date: Wed, 25 Sep 2024 17:45:21 +0300 Subject: [PATCH] fix: used to panic when attempting an unauthorized plugin operation --- src/access_control.rs | 17 +++++++++---- src/sql.rs | 10 ++++++++ test/int/test_plugin.py | 53 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/access_control.rs b/src/access_control.rs index 0a21d301d8..ad9cd17ecc 100644 --- a/src/access_control.rs +++ b/src/access_control.rs @@ -667,17 +667,24 @@ pub(super) fn access_check_op( Ok(()) } Op::Plugin { .. } => { - if !is_superuser(as_user) { - let sys_user = user_by_id(as_user)?; - #[rustfmt::skip] - return Err(BoxError::new(AccessDenied, format!("Plugin system access is denied for user '{}'", sys_user.name)).into()); - } + access_check_plugin_system(as_user)?; Ok(()) } Op::Acl(acl) => access_check_acl(storage, acl, as_user), } } +/// This function is also called from [`crate::sql::dispatch`] for client-side +/// permission checking. See comments next to it for details. +pub(crate) fn access_check_plugin_system(as_user: UserId) -> tarantool::Result<()> { + if !is_superuser(as_user) { + let sys_user = user_by_id(as_user)?; + #[rustfmt::skip] + return Err(BoxError::new(AccessDenied, format!("Plugin system access is denied for user '{}'", sys_user.name)).into()); + } + Ok(()) +} + // TODO: use this function everywhere we check `id != ADMIN_ID` #[inline(always)] pub fn is_superuser(user_id: UserId) -> bool { diff --git a/src/sql.rs b/src/sql.rs index 6b87de06ab..8a9f527055 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -1,5 +1,6 @@ //! Clusterwide SQL query execution. +use crate::access_control::access_check_plugin_system; use crate::access_control::{validate_password, UserMetadataKind}; use crate::cas::Predicate; use crate::schema::{ @@ -242,6 +243,15 @@ pub fn dispatch(mut query: Query<RouterRuntime>) -> traft::Result<Tuple> { Ok(duration_from_secs_f64_clamped(secs)) }; + // NOTE: this is different from how access checks are done for DDL, because: + // 1) We do not plan on allowing non-superusers to do plugin operations in the near future. + // 2) Preparing a plugin operation involves accessing a number of + // internal tables and potentially even running plugin code for + // validation purposes, so we exit early in case of denied access. + // See also <https://git.picodata.io/picodata/picodata/picodata/-/issues/965> + let as_user = effective_user_id(); + with_su(ADMIN_ID, || access_check_plugin_system(as_user))??; + match plugin { Plugin::Create(CreatePlugin { name, diff --git a/test/int/test_plugin.py b/test/int/test_plugin.py index bfb7699204..f315a669a5 100644 --- a/test/int/test_plugin.py +++ b/test/int/test_plugin.py @@ -2735,3 +2735,56 @@ def test_sql_interface_inheritance(cluster: Cluster): plugin_ref.assert_synced() cfg_space = plugin_ref.get_config("testservice_1", i1) assert cfg_space == {"foo": True, "bar": 101, "baz": ["one", "two", "three"]} + + +def test_plugin_sql_permission_denied(cluster: Cluster): + [i1] = cluster.deploy(instance_count=1) + + with pytest.raises(TarantoolError) as e: + i1.sql(f"CREATE PLUGIN {_PLUGIN} 0.1.0", user="guest") + assert e.value.args[:2] == ( + "ER_ACCESS_DENIED", + "Plugin system access is denied for user 'guest'", + ) + + # Access is checked before plugin name validation + with pytest.raises(TarantoolError) as e: + i1.sql("CREATE PLUGIN no_such_plugin 0.1.0", user="guest") + assert e.value.args[:2] == ( + "ER_ACCESS_DENIED", + "Plugin system access is denied for user 'guest'", + ) + + # Create as superuser to check the rest of commands + i1.sql(f"CREATE PLUGIN {_PLUGIN} 0.1.0") + + with pytest.raises(TarantoolError) as e: + i1.sql( + f'ALTER PLUGIN {_PLUGIN} 0.1.0 ADD SERVICE no_such_service TO TIER "{_DEFAULT_TIER}"', + user="guest", + ) + assert e.value.args[:2] == ( + "ER_ACCESS_DENIED", + "Plugin system access is denied for user 'guest'", + ) + + with pytest.raises(TarantoolError) as e: + i1.sql(f"ALTER PLUGIN {_PLUGIN} 0.1.0 ENABLE", user="guest") + assert e.value.args[:2] == ( + "ER_ACCESS_DENIED", + "Plugin system access is denied for user 'guest'", + ) + + with pytest.raises(TarantoolError) as e: + i1.sql(f"ALTER PLUGIN {_PLUGIN} 0.1.0 DISABLE", user="guest") + assert e.value.args[:2] == ( + "ER_ACCESS_DENIED", + "Plugin system access is denied for user 'guest'", + ) + + with pytest.raises(TarantoolError) as e: + i1.sql(f"DROP PLUGIN {_PLUGIN} 0.1.0", user="guest") + assert e.value.args[:2] == ( + "ER_ACCESS_DENIED", + "Plugin system access is denied for user 'guest'", + ) -- GitLab