From 9a1dbb8a6ee5a8e3d0b51c391b001104e29aa2a3 Mon Sep 17 00:00:00 2001 From: Kurdakov Alexander <kusancho12@gmail.com> Date: Wed, 22 Nov 2023 16:05:41 +0300 Subject: [PATCH] feat: populate builtins roles and users --- src/bootstrap_entries.rs | 73 +++++++++++++++++++++++++++++++++++++ src/schema.rs | 78 ++++++++++++++++++++++++++++++++++++++++ src/storage.rs | 27 +++++++++++--- test/int/test_acl.py | 36 ++++++++++++++++++- test/int/test_basics.py | 51 ++++++++++++++++++++------ test/int/test_sql.py | 19 ++++++---- 6 files changed, 262 insertions(+), 22 deletions(-) diff --git a/src/bootstrap_entries.rs b/src/bootstrap_entries.rs index 57db4adb87..56a81bcf69 100644 --- a/src/bootstrap_entries.rs +++ b/src/bootstrap_entries.rs @@ -1,8 +1,15 @@ use ::raft::prelude as raft; use protobuf::Message; +use tarantool::auth::AuthData; +use tarantool::auth::AuthDef; +use tarantool::auth::AuthMethod; +use tarantool::session::UserId; use crate::cli::args; use crate::instance::Instance; +use crate::schema::PrivilegeDef; +use crate::schema::RoleDef; +use crate::schema::UserDef; use crate::sql::pgproto; use crate::storage; use crate::storage::ClusterwideTable; @@ -12,6 +19,11 @@ use crate::traft; use crate::traft::op; use crate::traft::LogicalClock; +pub const GUEST_ID: UserId = 0; +pub const ADMIN_ID: UserId = 1; +pub const PUBLIC_ID: UserId = 2; +pub const SUPER_ID: UserId = 31; + pub(super) fn prepare(args: &args::Run, instance: &Instance, tiers: &[Tier]) -> Vec<raft::Entry> { let mut lc = LogicalClock::new(instance.raft_id, 0); let mut init_entries = Vec::new(); @@ -108,6 +120,67 @@ pub(super) fn prepare(args: &args::Run, instance: &Instance, tiers: &[Tier]) -> &(PropertyName::SnapshotReadViewCloseTimeout, storage::DEFAULT_SNAPSHOT_READ_VIEW_CLOSE_TIMEOUT), ) ); + // Populate system roles and their privileges to match tarantool ones + // Note: op::Dml is used instead of op::Acl because with Acl + // replicas will attempt to apply these records to coresponding + // tarantool spaces which is not needed + // equivalent SQL expression: CREATE USER 'guest' WITH PASSWORD '' USING chap-sha1 + init_entries_push_op(op::Dml::insert( + ClusterwideTable::User, + &UserDef { + id: GUEST_ID, + name: String::from("guest"), + schema_version: 0, + auth: AuthDef::new( + AuthMethod::ChapSha1, + AuthData::new(&AuthMethod::ChapSha1, "guest", "").into_string(), + ), + }, + )); + + // equivalent SQL expression: CREATE USER 'admin' with PASSWORD 'no password, see below for more details' USING chap-sha1 + init_entries_push_op(op::Dml::insert( + ClusterwideTable::User, + &UserDef { + id: ADMIN_ID, + name: String::from("admin"), + schema_version: 0, + // this is a bit different from vanilla tnt + // in vanilla tnt auth def is empty. Here for simplicity give navailable module api + // we use ChapSha with invalid password + // (its impossible to get empty string as output of sha1) + auth: AuthDef::new(AuthMethod::ChapSha1, String::from("")), + }, + )); + + // equivalent SQL expression: CREATE ROLE 'public' + init_entries_push_op(op::Dml::insert( + ClusterwideTable::Role, + &RoleDef { + id: PUBLIC_ID, + name: String::from("public"), + schema_version: 0, + }, + )); + + // equivalent SQL expression: CREATE ROLE 'super' + init_entries_push_op(op::Dml::insert( + ClusterwideTable::Role, + &RoleDef { + id: SUPER_ID, + name: String::from("super"), + schema_version: 0, + }, + )); + + // equivalent SQL expressions under 'admin' user: + // GRANT <'usage', 'session'> ON 'universe' TO 'guest' + // GRANT 'execute' ON <'public', 'super'> TO 'guest' + // GRANT 'all privileges' ON 'universe' TO 'admin' + // GRANT 'all privileges' ON 'universe' TO 'super' + for priv_def in PrivilegeDef::get_default_privileges() { + init_entries_push_op(op::Dml::insert(ClusterwideTable::Privilege, priv_def)); + } init_entries.push({ let conf_change = raft::ConfChange { diff --git a/src/schema.rs b/src/schema.rs index c71bf24654..674acf4f71 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,3 +1,4 @@ +use once_cell::sync::OnceCell; use std::borrow::Cow; use std::collections::{BTreeMap, HashSet}; use std::time::Duration; @@ -23,6 +24,7 @@ use tarantool::{ use serde::{Deserialize, Serialize}; +use crate::bootstrap_entries::{ADMIN_ID, GUEST_ID, SUPER_ID}; use crate::cas::{self, compare_and_swap}; use crate::storage::SPACE_ID_INTERNAL_MAX; use crate::storage::{ClusterwideTable, PropertyName}; @@ -369,6 +371,82 @@ impl PrivilegeDef { schema_version: 337, } } + + pub fn get_default_privileges() -> &'static Vec<PrivilegeDef> { + static DEFAULT_PRIVILEGES: OnceCell<Vec<PrivilegeDef>> = OnceCell::new(); + DEFAULT_PRIVILEGES.get_or_init(|| { + let mut v = Vec::new(); + + // SQL: GRANT <'usage', 'session'> ON 'universe' TO 'guest' + for privilege in ["usage", "session"] { + v.push(PrivilegeDef { + grantor_id: ADMIN_ID, + grantee_id: GUEST_ID, + object_type: String::from("universe"), + object_name: String::from(""), + privilege: String::from(privilege), + schema_version: 0, + }); + } + + // execute public + // execute on super (temporary until we switch to service account) + // SQL: GRANT 'execute' ON <'public', 'user'> TO 'guest' + for role in ["public", "super"] { + v.push(PrivilegeDef { + grantor_id: ADMIN_ID, + grantee_id: GUEST_ID, + object_type: String::from("role"), + object_name: String::from(role), + privilege: String::from("execute"), + schema_version: 0, + }); + } + + #[rustfmt::skip] + // TODO enum for privs implemented in picodata + let all_privileges = [ + "read", + "write", + "execute", + "session", + "usage", + "create", + "drop", + "alter", + "grant", + "revoke", + ]; + + // admin - all on universe + // SQL: GRANT 'all privileges' ON 'universe' TO 'admin' + for privilege in all_privileges { + v.push(PrivilegeDef { + grantor_id: ADMIN_ID, + grantee_id: ADMIN_ID, + object_type: String::from("universe"), + object_name: String::from(""), + privilege: String::from(privilege), + schema_version: 0, + }); + } + + // super - all on universe + // GRANT 'all privileges' ON 'universe' TO 'super' + for privilege in all_privileges { + v.push(PrivilegeDef { + grantor_id: ADMIN_ID, + grantee_id: SUPER_ID, + object_type: String::from("universe"), + object_name: String::from(""), + privilege: String::from(privilege), + schema_version: 0, + }); + } + + v + }) + } } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/storage.rs b/src/storage.rs index d719fdff22..9b5c0fde52 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -2483,6 +2483,21 @@ trait SchemaDef { fn on_delete(key: &Self::Key, storage: &Clusterwide) -> traft::Result<()>; } +/// used in `on_insert` of SchemaDef for `RoleDef` and `UserDef` to ignore errors when +/// inserting builtin tarantool users and roles +fn ignore_tuple_found_error(res: tarantool::Result<()>) -> traft::Result<()> { + if let Err(err) = res { + if let TntError::Tarantool(tnt_err) = &err { + if tnt_err.error_code() == tarantool::error::TarantoolErrorCode::TupleFound as u32 { + return Ok(()); + } + } + return Err(traft::error::Error::from(err)); + } + + Ok(()) +} + impl SchemaDef for TableDef { type Key = SpaceId; @@ -2540,8 +2555,8 @@ impl SchemaDef for UserDef { #[inline(always)] fn on_insert(&self, storage: &Clusterwide) -> traft::Result<()> { _ = storage; - acl::on_master_create_user(self)?; - Ok(()) + let res = acl::on_master_create_user(self); + ignore_tuple_found_error(res) } #[inline(always)] @@ -2568,8 +2583,8 @@ impl SchemaDef for RoleDef { #[inline(always)] fn on_insert(&self, storage: &Clusterwide) -> traft::Result<()> { _ = storage; - acl::on_master_create_role(self)?; - Ok(()) + let res = acl::on_master_create_role(self); + ignore_tuple_found_error(res) } #[inline(always)] @@ -2596,6 +2611,10 @@ impl SchemaDef for PrivilegeDef { #[inline(always)] fn on_insert(&self, storage: &Clusterwide) -> traft::Result<()> { _ = storage; + if Self::get_default_privileges().contains(self) { + return Ok(()); + } + acl::on_master_grant_privilege(self)?; Ok(()) } diff --git a/test/int/test_acl.py b/test/int/test_acl.py index 5301a8573a..4ab169a52b 100644 --- a/test/int/test_acl.py +++ b/test/int/test_acl.py @@ -828,6 +828,7 @@ def test_acl_from_snapshot(cluster: Cluster): def test_acl_drop_space_with_privileges(cluster: Cluster): i1, *_ = cluster.deploy(instance_count=1) + number_of_privileges_since_bootstrap = 24 # Check that we can drop a table with privileges granted on it. index = i1.call("pico.create_user", "Dave", VALID_PASSWORD) @@ -845,7 +846,40 @@ def test_acl_drop_space_with_privileges(cluster: Cluster): # Check that the picodata privileges are gone. privs = i1.call("box.execute", """ select count(*) from "_pico_privilege" """) - assert privs["rows"][0][0] == 0 + assert privs["rows"][0][0] == number_of_privileges_since_bootstrap + + +def test_builtin_users_and_roles(cluster: Cluster): + i1, *_ = cluster.deploy(instance_count=1) + + # validate that builtin users and roles can be referenced in pico.{grant,revoke}_privilege + for user in ["admin", "guest", "public", "super"]: + i1.call( + "pico.grant_privilege", + user, + "read", + "table", + "_pico_property", + ) + + index = i1.call("pico.raft_get_index") + new_index = i1.call( + "pico.grant_privilege", + "admin", + "write", + "universe", + ) + + assert index == new_index + + index = i1.call("pico.create_user", "Dave", VALID_PASSWORD) + new_index = i1.call( + "pico.grant_privilege", + "Dave", + "execute", + "role", + "super", + ) # TODO: test acl get denied when there's an unfinished ddl diff --git a/test/int/test_basics.py b/test/int/test_basics.py index ed9e6dc699..8f311c536c 100644 --- a/test/int/test_basics.py +++ b/test/int/test_basics.py @@ -251,16 +251,44 @@ def test_raft_log(instance: Instance): | 9 | 1 |1.0.9|Insert({_pico_property}, ["max_pg_portals",50])| | 10 | 1 |1.0.10|Insert({_pico_property}, ["snapshot_chunk_max_size",16777216])| | 11 | 1 |1.0.11|Insert({_pico_property}, ["snapshot_read_view_close_timeout",86400.0])| -| 12 | 1 | |AddNode(1)| -| 13 | 2 | |-| -| 14 | 2 |1.1.1|Replace({_pico_instance}, ["i1","68d4a766-4144-3248-aeb4-e212356716e4",1,"r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07",["Offline",0],["Online",1],{b},"default"])| -| 15 | 2 |1.1.2|Insert({_pico_replicaset}, ["r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07","i1","default",0.0,"auto","not-ready"])| -| 16 | 2 |1.1.3|Replace({_pico_instance}, ["i1","68d4a766-4144-3248-aeb4-e212356716e4",1,"r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07",["Replicated",1],["Online",1],{b},"default"])| -| 17 | 2 |1.1.4|Update({_pico_replicaset}, ["r1"], [["=","weight",1.0], ["=","state","ready"]])| -| 18 | 2 |1.1.5|Replace({_pico_property}, ["target_vshard_config",[{{"e0df68c5-e7f9-395f-86b3-30ad9e1b7b07":[{{"68d4a766-4144-3248-aeb4-e212356716e4":["guest:@127.0.0.1:{p}","i1",true]}},1.0]}},"on"]])| -| 19 | 2 |1.1.6|Replace({_pico_property}, ["current_vshard_config",[{{"e0df68c5-e7f9-395f-86b3-30ad9e1b7b07":[{{"68d4a766-4144-3248-aeb4-e212356716e4":["guest:@127.0.0.1:{p}","i1",true]}},1.0]}},"on"]])| -| 20 | 2 |1.1.7|Replace({_pico_property}, ["vshard_bootstrapped",true])| -| 21 | 2 |1.1.8|Replace({_pico_instance}, ["i1","68d4a766-4144-3248-aeb4-e212356716e4",1,"r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07",["Online",1],["Online",1],{b},"default"])| +| 12 | 1 |1.0.12|Insert({_pico_user}, [0,"guest",0,["chap-sha1","vhvewKp0tNyweZQ+cFKAlsyphfg="]])| +| 13 | 1 |1.0.13|Insert({_pico_user}, [1,"admin",0,["chap-sha1",""]])| +| 14 | 1 |1.0.14|Insert({_pico_role}, [2,"public",0])| +| 15 | 1 |1.0.15|Insert({_pico_role}, [31,"super",0])| +| 16 | 1 |1.0.16|Insert({_pico_privilege}, [1,0,"universe","","usage",0])| +| 17 | 1 |1.0.17|Insert({_pico_privilege}, [1,0,"universe","","session",0])| +| 18 | 1 |1.0.18|Insert({_pico_privilege}, [1,0,"role","public","execute",0])| +| 19 | 1 |1.0.19|Insert({_pico_privilege}, [1,0,"role","super","execute",0])| +| 20 | 1 |1.0.20|Insert({_pico_privilege}, [1,1,"universe","","read",0])| +| 21 | 1 |1.0.21|Insert({_pico_privilege}, [1,1,"universe","","write",0])| +| 22 | 1 |1.0.22|Insert({_pico_privilege}, [1,1,"universe","","execute",0])| +| 23 | 1 |1.0.23|Insert({_pico_privilege}, [1,1,"universe","","session",0])| +| 24 | 1 |1.0.24|Insert({_pico_privilege}, [1,1,"universe","","usage",0])| +| 25 | 1 |1.0.25|Insert({_pico_privilege}, [1,1,"universe","","create",0])| +| 26 | 1 |1.0.26|Insert({_pico_privilege}, [1,1,"universe","","drop",0])| +| 27 | 1 |1.0.27|Insert({_pico_privilege}, [1,1,"universe","","alter",0])| +| 28 | 1 |1.0.28|Insert({_pico_privilege}, [1,1,"universe","","grant",0])| +| 29 | 1 |1.0.29|Insert({_pico_privilege}, [1,1,"universe","","revoke",0])| +| 30 | 1 |1.0.30|Insert({_pico_privilege}, [1,31,"universe","","read",0])| +| 31 | 1 |1.0.31|Insert({_pico_privilege}, [1,31,"universe","","write",0])| +| 32 | 1 |1.0.32|Insert({_pico_privilege}, [1,31,"universe","","execute",0])| +| 33 | 1 |1.0.33|Insert({_pico_privilege}, [1,31,"universe","","session",0])| +| 34 | 1 |1.0.34|Insert({_pico_privilege}, [1,31,"universe","","usage",0])| +| 35 | 1 |1.0.35|Insert({_pico_privilege}, [1,31,"universe","","create",0])| +| 36 | 1 |1.0.36|Insert({_pico_privilege}, [1,31,"universe","","drop",0])| +| 37 | 1 |1.0.37|Insert({_pico_privilege}, [1,31,"universe","","alter",0])| +| 38 | 1 |1.0.38|Insert({_pico_privilege}, [1,31,"universe","","grant",0])| +| 39 | 1 |1.0.39|Insert({_pico_privilege}, [1,31,"universe","","revoke",0])| +| 40 | 1 | |AddNode(1)| +| 41 | 2 | |-| +| 42 | 2 |1.1.1|Replace({_pico_instance}, ["i1","68d4a766-4144-3248-aeb4-e212356716e4",1,"r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07",["Offline",0],["Online",1],{b},"default"])| +| 43 | 2 |1.1.2|Insert({_pico_replicaset}, ["r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07","i1","default",0.0,"auto","not-ready"])| +| 44 | 2 |1.1.3|Replace({_pico_instance}, ["i1","68d4a766-4144-3248-aeb4-e212356716e4",1,"r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07",["Replicated",1],["Online",1],{b},"default"])| +| 45 | 2 |1.1.4|Update({_pico_replicaset}, ["r1"], [["=","weight",1.0], ["=","state","ready"]])| +| 46 | 2 |1.1.5|Replace({_pico_property}, ["target_vshard_config",[{{"e0df68c5-e7f9-395f-86b3-30ad9e1b7b07":[{{"68d4a766-4144-3248-aeb4-e212356716e4":["guest:@127.0.0.1:{p}","i1",true]}},1.0]}},"on"]])| +| 47 | 2 |1.1.6|Replace({_pico_property}, ["current_vshard_config",[{{"e0df68c5-e7f9-395f-86b3-30ad9e1b7b07":[{{"68d4a766-4144-3248-aeb4-e212356716e4":["guest:@127.0.0.1:{p}","i1",true]}},1.0]}},"on"]])| +| 48 | 2 |1.1.7|Replace({_pico_property}, ["vshard_bootstrapped",true])| +| 49 | 2 |1.1.8|Replace({_pico_instance}, ["i1","68d4a766-4144-3248-aeb4-e212356716e4",1,"r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07",["Online",1],["Online",1],{b},"default"])| +-----+----+-----+--------+ """.format( # noqa: E501 p=instance.port, @@ -270,6 +298,9 @@ def test_raft_log(instance: Instance): _pico_replicaset=space_id("_pico_replicaset"), _pico_instance=space_id("_pico_instance"), _pico_tier=space_id("_pico_tier"), + _pico_privilege=space_id("_pico_privilege"), + _pico_user=space_id("_pico_user"), + _pico_role=space_id("_pico_role"), ) assert strip_spaces(expected) == strip_spaces(raft_log) diff --git a/test/int/test_sql.py b/test/int/test_sql.py index 660e426c6f..0354069039 100644 --- a/test/int/test_sql.py +++ b/test/int/test_sql.py @@ -568,6 +568,11 @@ def test_sql_acl_user(cluster: Cluster): upper_username = "USER" rolename = "Role" upper_rolename = "ROLE" + default_users = [ + [0, "guest", 0, ["chap-sha1", "vhvewKp0tNyweZQ+cFKAlsyphfg="]], + [1, "admin", 0, ["chap-sha1", ""]], + ] + default_roles = [[2, "public", 0], [31, "super", 0]] acl = i1.sql( f""" create user "{username}" with password '{password}' @@ -583,7 +588,7 @@ def test_sql_acl_user(cluster: Cluster): # Dropping user that does exist should return 1. acl = i1.sql(f'drop user "{username}"') assert acl["row_count"] == 1 - assert i1.call("box.space._pico_user:select") == [] + assert i1.call("box.space._pico_user:select") == default_users # All the usernames below should match the same user. # * Upcasted username in double parentheses shouldn't change. @@ -688,16 +693,16 @@ def test_sql_acl_user(cluster: Cluster): # Check altering works. acl = i1.sql(f"create user {username} with password '{password}' using md5") assert acl["row_count"] == 1 - users_auth_was = i1.call("box.space._pico_user:select")[0][3] + users_auth_was = i1.call("box.space._pico_user:select")[2][3] # * Password and method aren't changed -> update nothing. acl = i1.sql(f"alter user {username} with password '{password}' using md5") assert acl["row_count"] == 1 - users_auth_became = i1.call("box.space._pico_user:select")[0][3] + users_auth_became = i1.call("box.space._pico_user:select")[2][3] assert users_auth_was == users_auth_became # * Password is changed -> update hash. acl = i1.sql(f"alter user {username} with password '{another_password}' using md5") assert acl["row_count"] == 1 - users_auth_became = i1.call("box.space._pico_user:select")[0][3] + users_auth_became = i1.call("box.space._pico_user:select")[2][3] assert users_auth_was[0] == users_auth_became[0] assert users_auth_was[1] != users_auth_became[1] # * Password and method are changed -> update method and hash. @@ -705,13 +710,13 @@ def test_sql_acl_user(cluster: Cluster): f"alter user {username} with password '{another_password}' using chap-sha1" ) assert acl["row_count"] == 1 - users_auth_became = i1.call("box.space._pico_user:select")[0][3] + users_auth_became = i1.call("box.space._pico_user:select")[2][3] assert users_auth_was[0] != users_auth_became[0] assert users_auth_was[1] != users_auth_became[1] # * LDAP should ignore password -> update method and hash. acl = i1.sql(f"alter user {username} with password '{another_password}' using ldap") assert acl["row_count"] == 1 - users_auth_became = i1.call("box.space._pico_user:select")[0][3] + users_auth_became = i1.call("box.space._pico_user:select")[2][3] assert users_auth_was[0] != users_auth_became[0] assert users_auth_became[1] == "" acl = i1.sql(f"drop user {username}") @@ -744,7 +749,7 @@ def test_sql_acl_user(cluster: Cluster): # Dropping role that does exist should return 1. acl = i1.sql(f'drop role "{rolename}"') assert acl["row_count"] == 1 - assert i1.call("box.space._pico_role:select") == [] + assert i1.call("box.space._pico_role:select") == default_roles # All the rolenames below should match the same role. acl = i1.sql(f'create role "{upper_rolename}"') -- GitLab