From 8e2e1991c9df4356e5959e36bc701092243aa36b Mon Sep 17 00:00:00 2001 From: Egor Ivkov <e.ivkov@picodata.io> Date: Fri, 17 May 2024 21:10:21 +0300 Subject: [PATCH] feat: allow user owner to grant and revoke login permission (cherry picked from commit 9c462099ff126506001f70c3153057003463d0e3) Conflicts: src/access_control.rs --- src/access_control.rs | 172 +++++++++++++++++++++++++++++++++++++++--- src/schema.rs | 2 +- src/sql.rs | 2 +- test/int/test_sql.py | 66 ++++++++++++++-- 4 files changed, 224 insertions(+), 18 deletions(-) diff --git a/src/access_control.rs b/src/access_control.rs index 440c75774d..a4540b93e6 100644 --- a/src/access_control.rs +++ b/src/access_control.rs @@ -352,26 +352,49 @@ fn access_check_grant_revoke( Some(object_id) => object_id, }; + // Nobody should be able to revoke privileges from pico_service + // As it would break the cluster permanently. + if access == PrivType::Revoke && priv_def.grantee_id() == PICO_SERVICE_ID { + tarantool::set_error!( + tarantool::error::TarantoolErrorCode::AccessDenied, + "Revoke '{}' from '{}' is denied for all users", + priv_def.privilege(), + PICO_SERVICE_USER_NAME, + ); + return Err(tarantool::error::TarantoolError::last().into()); + } + match priv_def.object_type() { PicoSchemaObjectType::Universe => { - // Only admin can grant on universe - if grantor_id != ADMIN_ID { + if priv_def.privilege() != PrivilegeType::Login { + // This assumption is used in the following checks + crate::warn_or_panic!( + "Only Login privilege can be granted on Universe, got {}", + priv_def.privilege() + ); return Err(make_access_denied( access_name, PicoSchemaObjectType::Universe, - "", + "Only Login privilege can be granted on Universe", grantor.name, )); } - // Even admin should not be able to revoke privileges from pico_service - // As it would break the cluster permanently. - if access == PrivType::Revoke && priv_def.grantee_id() == PICO_SERVICE_ID { + // Following checks are assuming that it's a grant or revoke of Login privilege + + let target_sys_user = user_by_id(priv_def.grantee_id())?; + if target_sys_user.ty != UserMetadataKind::User { + return Err(make_no_such_user(&target_sys_user.name)); + } + + // Only owner or admin can grant login on user + if target_sys_user.owner_id != grantor_id && grantor_id != ADMIN_ID { tarantool::set_error!( tarantool::error::TarantoolErrorCode::AccessDenied, - "Revoke '{}' from '{}' is denied for all users", - priv_def.privilege(), - PICO_SERVICE_USER_NAME, + "{} Login from '{}' is denied for {}", + access_name, + target_sys_user.name, + grantor.name ); return Err(tarantool::error::TarantoolError::last().into()); } @@ -615,7 +638,7 @@ mod tests { access_control::{access_check_op, UserMetadataKind}, schema::{ auth_for_role_definition, Distribution, PrivilegeDef, PrivilegeType, SchemaObjectType, - UserDef, ADMIN_ID, + UserDef, ADMIN_ID, UNIVERSE_ID, }, storage::{ acl::{ @@ -1585,4 +1608,133 @@ mod tests { ); } } + + #[tarantool::test] + fn alter_user_login() { + let storage = Clusterwide::for_tests(); + + let owner_name = "test_owner"; + let user_name = "test_user"; + + let owner_id = make_user(owner_name, None); + let user_id = make_user(user_name, Some(owner_id)); + + // User that is not an owner or admin cannot grant/revoke login on user + let e = access_check_acl( + &storage, + &Acl::GrantPrivilege { + priv_def: PrivilegeDef::new( + PrivilegeType::Login, + SchemaObjectType::Universe, + UNIVERSE_ID, + user_id, + user_id, + 0, + ) + .unwrap(), + }, + user_id, + ) + .unwrap_err(); + assert_eq!( + e.to_string(), + format!( + "box error: AccessDenied: Grant Login from '{user_name}' is denied for test_user" + ) + ); + + let e = access_check_acl( + &storage, + &Acl::RevokePrivilege { + priv_def: PrivilegeDef::new( + PrivilegeType::Login, + SchemaObjectType::Universe, + UNIVERSE_ID, + user_id, + user_id, + 0, + ) + .unwrap(), + initiator: user_id, + }, + user_id, + ) + .unwrap_err(); + assert_eq!( + e.to_string(), + format!( + "box error: AccessDenied: Revoke Login from '{user_name}' is denied for test_user" + ) + ); + + // Owner can grant/revoke login on user + access_check_acl( + &storage, + &Acl::GrantPrivilege { + priv_def: PrivilegeDef::new( + PrivilegeType::Login, + SchemaObjectType::Universe, + UNIVERSE_ID, + user_id, + owner_id, + 0, + ) + .unwrap(), + }, + owner_id, + ) + .unwrap(); + access_check_acl( + &storage, + &Acl::RevokePrivilege { + priv_def: PrivilegeDef::new( + PrivilegeType::Login, + SchemaObjectType::Universe, + UNIVERSE_ID, + user_id, + owner_id, + 0, + ) + .unwrap(), + initiator: owner_id, + }, + owner_id, + ) + .unwrap(); + + // Admin can grant/revoke login on user + access_check_acl( + &storage, + &Acl::GrantPrivilege { + priv_def: PrivilegeDef::new( + PrivilegeType::Login, + SchemaObjectType::Universe, + UNIVERSE_ID, + user_id, + ADMIN_ID, + 0, + ) + .unwrap(), + }, + ADMIN_ID, + ) + .unwrap(); + access_check_acl( + &storage, + &Acl::RevokePrivilege { + priv_def: PrivilegeDef::new( + PrivilegeType::Login, + SchemaObjectType::Universe, + UNIVERSE_ID, + user_id, + ADMIN_ID, + 0, + ) + .unwrap(), + initiator: ADMIN_ID, + }, + ADMIN_ID, + ) + .unwrap(); + } } diff --git a/src/schema.rs b/src/schema.rs index a86f04f937..b7bbace3f3 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -474,7 +474,7 @@ impl PrivilegeType { /// user Id of the vanilla role super we keep it as a placeholder. Aside from that /// vanilla super role has some privileges that cannot be represented in picodata, /// namely privileges on universe. This hole opens possibility for unwanted edge cases. -/// 2) Only Session and Usage can be granted on Universe. +/// 2) Only Login can be granted on Universe in picodata. /// Note that validation is not performed in Deserialize. We assume that for untrusted data /// the object is created via constructor. In other cases validation was already performed /// prior to serialization thus deserialization always creates a valid object. diff --git a/src/sql.rs b/src/sql.rs index 3ca028f8dc..76c84d26e7 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -1228,7 +1228,7 @@ fn reenterable_schema_change_request( }; // For ALTER Login/NoLogin. - let grantor_id = session::euid()?; + let grantor_id = current_user; let grantee_id = get_grantee_id(storage, name)?; let object_type = SchemaObjectType::Universe; let object_id = 0; diff --git a/test/int/test_sql.py b/test/int/test_sql.py index 3c49931afb..6a7170af12 100644 --- a/test/int/test_sql.py +++ b/test/int/test_sql.py @@ -1738,20 +1738,34 @@ def test_sql_alter_login(cluster: Cluster): cluster.deploy(instance_count=2) i1, i2 = cluster.instances + # Create the owner of `USER` + owner_username = "OWNER_USER" + owner_password = "PA5sWORD" + + acl = i1.sudo_sql(f"create user {owner_username} with password '{owner_password}'") + assert acl["row_count"] == 1 + + acl = i1.sudo_sql(f"grant create user to {owner_username}") + assert acl["row_count"] == 1 + username = "USER" password = "PA5sWORD" # Create user. - acl = i1.sudo_sql(f"create user {username} with password '{password}'") + acl = i1.sql( + f"create user {username} with password '{password}'", + user=owner_username, + password=owner_password, + ) assert acl["row_count"] == 1 # Alter user with LOGIN option - opertaion is idempotent. - acl = i1.sudo_sql(f""" alter user {username} with login """) + acl = i1.sudo_sql(f"alter user {username} with login") assert acl["row_count"] == 1 # Alter user with NOLOGIN option. - acl = i1.sudo_sql(f""" alter user {username} with nologin """) + acl = i1.sudo_sql(f"alter user {username} with nologin") assert acl["row_count"] == 1 # Alter user with NOLOGIN again - operation is idempotent. - acl = i1.sudo_sql(f""" alter user {username} with nologin """) + acl = i1.sudo_sql(f"alter user {username} with nologin") assert acl["row_count"] == 1 # Login privilege is removed with pytest.raises( @@ -1760,6 +1774,47 @@ def test_sql_alter_login(cluster: Cluster): ): i1.sql("insert into t values(2);", user=username, password=password) + # Alter user with LOGIN option again - `USER` owner can also modify LOGIN privilege. + acl = i1.sql( + f"alter user {username} with login", + user=owner_username, + password=owner_password, + ) + assert acl["row_count"] == 1 + + # Alter user with NOLOGIN option again - `USER` owner can also modify LOGIN privilege. + acl = i1.sql( + f"alter user {username} with nologin", + user=owner_username, + password=owner_password, + ) + assert acl["row_count"] == 1 + + # Login privilege cannot be granted or removed by any user + # other than admin or user account owner + other_username = "OTHER_USER" + other_password = "PA5sWORD" + acl = i1.sudo_sql(f"create user {other_username} with password '{other_password}'") + assert acl["row_count"] == 1 + with pytest.raises( + Exception, + match=f"AccessDenied: Grant Login from '{username}' is denied for {other_username}", + ): + acl = i1.sql( + f"alter user {username} with login", + user=other_username, + password=other_password, + ) + with pytest.raises( + Exception, + match=f"AccessDenied: Revoke Login from '{username}' is denied for {other_username}", + ): + acl = i1.sql( + f"alter user {username} with nologin", + user=other_username, + password=other_password, + ) + # Login privilege cannot be removed from pico_service even by admin. with pytest.raises( Exception, @@ -2170,7 +2225,7 @@ def test_sql_privileges(cluster: Cluster): username = "alice" alice_pwd = "Pa55sword" - # Create user with execute on universe privilege + # Create user acl = i1.sql( f""" create user "{username}" with password '{alice_pwd}' @@ -2178,7 +2233,6 @@ def test_sql_privileges(cluster: Cluster): """ ) assert acl["row_count"] == 1 - i1.eval(f""" pico.grant_privilege("{username}", "execute", "universe") """) # ------------------------ # Check SQL read privilege -- GitLab