diff --git a/src/cas.rs b/src/cas.rs index facfd485bf1577d4aa72c4788e576eb942d4b972..523849b2c47b9976480cb83f51dca90cb57b8b15 100644 --- a/src/cas.rs +++ b/src/cas.rs @@ -159,7 +159,7 @@ fn proc_cas_local(req: Request) -> Result<Response> { }); } if status.leader_id != Some(node.raft_id()) { - // Nearly impossible error indicating invalid request. + // Invalid request. This node is not a leader at this moment. return Err(TraftError::NotALeader); } @@ -274,23 +274,31 @@ fn proc_cas_local(req: Request) -> Result<Response> { req.predicate.check_entry(entry.index, &op, storage)?; } - if let Op::Dml(dml) = &req.op { - // Check if the requested dml is applicable to the local storage. - // This will run the required on_replace triggers which will check among - // other things conformity to space format, user defined constraints etc. - // - // FIXME: this works for explicit constraints which would be directly - // violated by the operation, but there are still some cases where an - // invalid dml operation would be proposed to the raft log, which would - // fail to apply, e.g. if there's a number of dml operations in flight - // which conflict via the secondary key. - // To fix these cases we would need to implement the so-called "limbo". - // See https://git.picodata.io/picodata/picodata/picodata/-/issues/368 - transaction::begin()?; - let res = storage.do_dml(dml); - transaction::rollback().expect("can't fail"); - // Return the error if it happened. Ignore the tuple if there was one. - _ = res?; + // Performs preliminary checks on an operation so that it will not fail when applied. + match &req.op { + Op::Dml(dml) => { + // Check if the requested dml is applicable to the local storage. + // This will run the required on_replace triggers which will check among + // other things conformity to space format, user defined constraints etc. + // + // FIXME: this works for explicit constraints which would be directly + // violated by the operation, but there are still some cases where an + // invalid dml operation would be proposed to the raft log, which would + // fail to apply, e.g. if there's a number of dml operations in flight + // which conflict via the secondary key. + // To fix these cases we would need to implement the so-called "limbo". + // See https://git.picodata.io/picodata/picodata/picodata/-/issues/368 + transaction::begin()?; + let res = storage.do_dml(dml); + transaction::rollback().expect("can't fail"); + // Return the error if it happened. Ignore the tuple if there was one. + _ = res?; + } + // We can't use a `do_acl` inside of a transaction here similarly to `do_dml`, + // as acl should be applied only on replicaset leader. Raft leader is not always + // a replicaset leader. + Op::Acl(acl) => acl.validate()?, + _ => (), } // Don't wait for the proposal to be accepted, instead return the index diff --git a/src/traft/op.rs b/src/traft/op.rs index 9d99d5cd9c657bc7fddb119aa38e15912da103e1..5dd9fe48666556d0881e2a1ab3364c0f11fc2b9a 100644 --- a/src/traft/op.rs +++ b/src/traft/op.rs @@ -1,6 +1,9 @@ -use crate::schema::{Distribution, PrivilegeDef, RoleDef, UserDef}; +use crate::schema::{ + Distribution, PrivilegeDef, RoleDef, UserDef, ADMIN_ID, GUEST_ID, PUBLIC_ID, SUPER_ID, +}; use crate::storage::space_by_name; use crate::storage::Clusterwide; +use crate::traft::error::Error as TRaftError; use ::tarantool::auth::AuthDef; use ::tarantool::index::{IndexId, Part}; use ::tarantool::space::{Field, SpaceId}; @@ -593,6 +596,40 @@ impl Acl { Self::RevokePrivilege { priv_def, .. } => priv_def.schema_version(), } } + + /// Performs preliminary checks on acl so that it will not fail when applied. + /// These checks do not include authorization checks, which are done separately in + /// [`crate::access_control::access_check_op`]. + pub fn validate(&self) -> Result<(), TRaftError> { + // THOUGHT: should we move access_check_* fns here as it's done in tarantool? + match self { + Self::ChangeAuth { user_id, .. } => { + // See https://git.picodata.io/picodata/tarantool/-/blob/da5ad0fa3ab8940f524cfa9bf3d582347c01fc4a/src/box/alter.cc#L2925 + if *user_id == GUEST_ID { + return Err(TRaftError::other( + "altering guest user's password is not allowed", + )); + } + } + Self::DropUser { user_id, .. } => { + // See https://git.picodata.io/picodata/tarantool/-/blob/da5ad0fa3ab8940f524cfa9bf3d582347c01fc4a/src/box/alter.cc#L3080 + if *user_id == GUEST_ID || *user_id == ADMIN_ID { + return Err(TRaftError::other("dropping system user is not allowed")); + } + // user_has_data will be successful in any case https://git.picodata.io/picodata/tarantool/-/blob/da5ad0fa3ab8940f524cfa9bf3d582347c01fc4a/src/box/alter.cc#L2846 + // as box.schema.user.drop(..) deletes all the related spaces/priveleges/etc. + } + + Self::DropRole { role_id, .. } => { + // See https://git.picodata.io/picodata/tarantool/-/blob/da5ad0fa3ab8940f524cfa9bf3d582347c01fc4a/src/box/alter.cc#L3080 + if *role_id == PUBLIC_ID || *role_id == SUPER_ID { + return Err(TRaftError::other("dropping system role is not allowed")); + } + } + _ => (), + } + Ok(()) + } } mod vec_of_raw_byte_buf { diff --git a/test/int/test_acl.py b/test/int/test_acl.py index 2182fe67a39222e8c7bd6f46e79b1989cba28e94..b09153f432d39fd668d900e3862db1662097adb0 100644 --- a/test/int/test_acl.py +++ b/test/int/test_acl.py @@ -1053,5 +1053,27 @@ def test_circular_grants_for_role(cluster: Cluster): throws_on_grant("Son", "Son") +def test_alter_system_user(cluster: Cluster): + i1, *_ = cluster.deploy(instance_count=1) + + with pytest.raises( + ReturnError, + match="altering guest user's password is not allowed", + ): + i1.sql("alter user \"guest\" with password '12345678'") + + with pytest.raises( + ReturnError, + match="dropping system user is not allowed", + ): + i1.sql('drop user "guest"') + + with pytest.raises( + ReturnError, + match="dropping system role is not allowed", + ): + i1.sql('drop role "public"') + + # TODO: test acl get denied when there's an unfinished ddl # TODO: check various retryable cas outcomes when doing schema change requests