From c4d37819077077b11004893f08dd270209bff79d Mon Sep 17 00:00:00 2001 From: Egor Ivkov <e.o.ivkov@gmail.com> Date: Tue, 21 Nov 2023 16:40:42 +0300 Subject: [PATCH] refactor: use object_id instead of object_name in privilege --- src/luamod.lua | 28 +++++++---- src/schema.rs | 49 ++++++++++++------ src/storage.rs | 108 ++++++++++++++++++++-------------------- src/traft/op.rs | 8 +-- test/int/test_acl.py | 39 ++++++++++++--- test/int/test_basics.py | 48 +++++++++--------- 6 files changed, 165 insertions(+), 115 deletions(-) diff --git a/src/luamod.lua b/src/luamod.lua index a124a4e038..84738fac3c 100644 --- a/src/luamod.lua +++ b/src/luamod.lua @@ -844,9 +844,12 @@ function pico.grant_privilege(grantee, privilege, object_type, object_name, opts end -- Throws error if object doesn't exist - object_resolve(object_type, object_name) + local object_id = object_resolve(object_type, object_name) + if object_id == "" then + object_id = -1 + end - if box.space._pico_privilege:get{grantee_def.id, object_type, object_name, privilege} ~= nil then + if box.space._pico_privilege:get{grantee_def.id, object_type, object_id, privilege} ~= nil then -- Privilege is already granted, no op needed return nil end @@ -855,11 +858,11 @@ function pico.grant_privilege(grantee, privilege, object_type, object_name, opts kind = 'acl', op_kind = 'grant_privilege', priv_def = { - grantor_id = box.session.uid(), - grantee_id = grantee_def.id, - object_type = object_type, - object_name = object_name, privilege = privilege, + object_type = object_type, + object_id = object_id, + grantee_id = grantee_def.id, + grantor_id = box.session.uid(), schema_version = next_schema_version(), }, } @@ -945,9 +948,12 @@ function pico.revoke_privilege(grantee, privilege, object_type, object_name, opt end -- Throws error if object doesn't exist - object_resolve(object_type, object_name) + local object_id = object_resolve(object_type, object_name) + if object_id == "" then + object_id = -1 + end - local priv = box.space._pico_privilege:get{grantee_def.id, object_type, object_name, privilege} + local priv = box.space._pico_privilege:get{grantee_def.id, object_type, object_id, privilege} if priv == nil then -- Privilege is not yet granted, no op needed return nil @@ -957,11 +963,11 @@ function pico.revoke_privilege(grantee, privilege, object_type, object_name, opt kind = 'acl', op_kind = 'revoke_privilege', priv_def = { + privilege = privilege, + object_type = object_type, + object_id = object_id, grantee_id = grantee_def.id, grantor_id = priv.grantor_id, - object_type = object_type, - object_name = object_name, - privilege = privilege, schema_version = next_schema_version(), }, } diff --git a/src/schema.rs b/src/schema.rs index 674acf4f71..59a433ab22 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -24,7 +24,7 @@ use tarantool::{ use serde::{Deserialize, Serialize}; -use crate::bootstrap_entries::{ADMIN_ID, GUEST_ID, SUPER_ID}; +use crate::bootstrap_entries::{ADMIN_ID, GUEST_ID, PUBLIC_ID, SUPER_ID}; use crate::cas::{self, compare_and_swap}; use crate::storage::SPACE_ID_INTERNAL_MAX; use crate::storage::{ClusterwideTable, PropertyName}; @@ -330,15 +330,22 @@ impl RoleDef { /// Privilege definition. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct PrivilegeDef { + pub privilege: String, + pub object_type: String, + /// `-1` denotes an absense of a target object. + /// Other values should be >= 0 and denote an existing target object. + /// When working with `object_type` `universe` it might seem that it does + /// not have a target object and `object_id` should be `-1`, this is incorrect + /// universe has a target object with `object_id == 0`. + /// + /// To get the value of this field as `Option<u32>` see [`Self::object_id`] + pub object_id: i64, /// Id of the user or role to whom the privilege is granted. /// /// In tarantool users and roles are stored in the same space, which means a /// role and a user cannot have the same id or name. - pub grantor_id: UserId, pub grantee_id: UserId, - pub object_type: String, - pub object_name: String, - pub privilege: String, + pub grantor_id: UserId, pub schema_version: u64, } @@ -350,11 +357,11 @@ impl PrivilegeDef { pub fn format() -> Vec<tarantool::space::Field> { use tarantool::space::Field; vec![ - Field::from(("grantor_id", FieldType::Unsigned)), - Field::from(("grantee_id", FieldType::Unsigned)), - Field::from(("object_type", FieldType::String)), - Field::from(("object_name", FieldType::String)), Field::from(("privilege", FieldType::String)), + Field::from(("object_type", FieldType::String)), + Field::from(("object_id", FieldType::Integer)), + Field::from(("grantee_id", FieldType::Unsigned)), + Field::from(("grantor_id", FieldType::Unsigned)), Field::from(("schema_version", FieldType::Unsigned)), ] } @@ -366,12 +373,23 @@ impl PrivilegeDef { grantor_id: 13, grantee_id: 37, object_type: "fruit".into(), - object_name: "banana".into(), + object_id: -1, privilege: "bite".into(), schema_version: 337, } } + /// Get `object_id` field interpreting `-1` as `None`. + #[inline(always)] + pub fn object_id(&self) -> Option<u32> { + if self.object_id >= 0 { + Some(self.object_id as _) + } else { + debug_assert_eq!(self.object_id, -1, "object_id should be >= -1"); + None + } + } + pub fn get_default_privileges() -> &'static Vec<PrivilegeDef> { static DEFAULT_PRIVILEGES: OnceCell<Vec<PrivilegeDef>> = OnceCell::new(); DEFAULT_PRIVILEGES.get_or_init(|| { @@ -383,7 +401,8 @@ impl PrivilegeDef { grantor_id: ADMIN_ID, grantee_id: GUEST_ID, object_type: String::from("universe"), - object_name: String::from(""), + // `universe` has object_id 0 + object_id: 0, privilege: String::from(privilege), schema_version: 0, }); @@ -392,12 +411,12 @@ impl PrivilegeDef { // 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"] { + for role in [PUBLIC_ID, SUPER_ID] { v.push(PrivilegeDef { grantor_id: ADMIN_ID, grantee_id: GUEST_ID, object_type: String::from("role"), - object_name: String::from(role), + object_id: role as _, privilege: String::from("execute"), schema_version: 0, }); @@ -425,7 +444,7 @@ impl PrivilegeDef { grantor_id: ADMIN_ID, grantee_id: ADMIN_ID, object_type: String::from("universe"), - object_name: String::from(""), + object_id: 0, privilege: String::from(privilege), schema_version: 0, }); @@ -438,7 +457,7 @@ impl PrivilegeDef { grantor_id: ADMIN_ID, grantee_id: SUPER_ID, object_type: String::from("universe"), - object_name: String::from(""), + object_id: 0, privilege: String::from(privilege), schema_version: 0, }); diff --git a/src/storage.rs b/src/storage.rs index 26c738937a..0db1949dfb 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -1905,11 +1905,9 @@ pub fn ddl_meta_space_update_operable( /// /// This function is called when applying the different ddl operations. pub fn ddl_meta_drop_space(storage: &Clusterwide, space_id: SpaceId) -> traft::Result<()> { - if let Some(space_name) = storage.tables.get(space_id)?.map(|s| s.name) { - storage - .privileges - .delete_all_by_object("space", &space_name)?; - } + storage + .privileges + .delete_all_by_object("space", space_id as i64)?; let iter = storage.indexes.by_space_id(space_id)?; for index in iter { storage.indexes.delete(index.table_id, index.id)?; @@ -2263,7 +2261,7 @@ impl Privileges { let primary_key = space .index_builder("primary") .unique(true) - .parts(["grantee_id", "object_type", "object_name", "privilege"]) + .parts(["grantee_id", "object_type", "object_id", "privilege"]) .if_not_exists(true) .create()?; @@ -2271,7 +2269,7 @@ impl Privileges { .index_builder("object") .unique(false) .part("object_type") - .part("object_name") + .part("object_id") .if_not_exists(true) .create()?; @@ -2287,9 +2285,9 @@ impl Privileges { &self, grantee_id: UserId, object_type: &str, - object_name: &str, + object_id: i64, ) -> tarantool::Result<Option<PrivilegeDef>> { - let tuple = self.space.get(&(grantee_id, object_type, object_name))?; + let tuple = self.space.get(&(grantee_id, object_type, object_id))?; tuple.as_ref().map(Tuple::decode).transpose() } @@ -2303,11 +2301,11 @@ impl Privileges { pub fn by_object( &self, object_type: &str, - object_name: &str, + object_id: i64, ) -> tarantool::Result<EntryIter<PrivilegeDef>> { let iter = self .object_idx - .select(IteratorType::Eq, &(object_type, object_name))?; + .select(IteratorType::Eq, &(object_type, object_id))?; Ok(EntryIter::new(iter)) } @@ -2328,11 +2326,11 @@ impl Privileges { &self, grantee_id: UserId, object_type: &str, - object_name: &str, + object_id: i64, privilege: &str, ) -> tarantool::Result<()> { self.space - .delete(&(grantee_id, object_type, object_name, privilege))?; + .delete(&(grantee_id, object_type, object_id, privilege))?; Ok(()) } @@ -2343,7 +2341,7 @@ impl Privileges { self.delete( priv_def.grantee_id, &priv_def.object_type, - &priv_def.object_name, + priv_def.object_id, &priv_def.privilege, )?; } @@ -2352,16 +2350,16 @@ impl Privileges { /// Remove any privilege definitions assigning the given role. #[inline] - pub fn delete_all_by_granted_role(&self, role_name: &str) -> Result<()> { + pub fn delete_all_by_granted_role(&self, role_id: u32) -> Result<()> { for priv_def in self.iter()? { if priv_def.privilege == "execute" && priv_def.object_type == "role" - && priv_def.object_name == role_name + && priv_def.object_id == role_id as i64 { self.delete( priv_def.grantee_id, &priv_def.object_type, - &priv_def.object_name, + priv_def.object_id, &priv_def.privilege, )?; } @@ -2371,16 +2369,12 @@ impl Privileges { /// Remove any privilege definitions granted to the given object. #[inline] - pub fn delete_all_by_object( - &self, - object_type: &str, - object_name: &str, - ) -> tarantool::Result<()> { - for priv_def in self.by_object(object_type, object_name)? { + pub fn delete_all_by_object(&self, object_type: &str, object_id: i64) -> tarantool::Result<()> { + for priv_def in self.by_object(object_type, object_id)? { self.delete( priv_def.grantee_id, &priv_def.object_type, - &priv_def.object_name, + priv_def.object_id, &priv_def.privilege, )?; } @@ -2732,24 +2726,18 @@ pub mod acl { /// Remove a role definition and any entities owned by it from the internal /// clusterwide storage. pub fn global_drop_role(storage: &Clusterwide, role_id: UserId) -> Result<()> { + let role_def = storage.roles.by_id(role_id)?.expect("role should exist"); storage.privileges.delete_all_by_grantee_id(role_id)?; + storage.privileges.delete_all_by_granted_role(role_id)?; + storage.roles.delete(role_id)?; - if let Some(role_def) = storage.roles.by_id(role_id)? { - // Revoke the role from any grantees. - storage - .privileges - .delete_all_by_granted_role(&role_def.name)?; - storage.roles.delete(role_id)?; - - let role = &role_def.name; - crate::audit!( - message: "dropped role `{role}`", - title: "drop_role", - severity: Medium, - role: role, - ); - } - + let role = &role_def.name; + crate::audit!( + message: "dropped role `{role}`", + title: "drop_role", + severity: Medium, + role: role, + ); Ok(()) } @@ -2761,11 +2749,12 @@ pub mod acl { storage.privileges.insert(priv_def)?; let privilege = &priv_def.privilege; - let (object, object_type) = (&priv_def.object_name, &priv_def.object_type); + let (object, object_type) = (priv_def.object_id(), &priv_def.object_type); let (grantee_type, grantee) = priv_def.grantee_type_and_name(storage)?; match (privilege.as_str(), object_type.as_str()) { ("execute", "role") => { + let object = object.expect("should be set"); crate::audit!( message: "granted role `{object}` to {grantee_type} `{grantee}`", title: "grant_role", @@ -2776,13 +2765,17 @@ pub mod acl { ); } _ => { + let object = match object { + Some(object) => format!("`{object}` "), + None => "".into(), + }; crate::audit!( - message: "granted privilege {privilege} on {object_type} `{object}` \ + message: "granted privilege {privilege} on {object_type} {object}\ to {grantee_type} `{grantee}`", title: "grant_privilege", severity: High, privilege: privilege, - object: object, + object: priv_def.object_id(), object_type: object_type, grantee: &grantee, grantee_type: grantee_type, @@ -2802,16 +2795,17 @@ pub mod acl { storage.privileges.delete( priv_def.grantee_id, &priv_def.object_type, - &priv_def.object_name, + priv_def.object_id, &priv_def.privilege, )?; let privilege = &priv_def.privilege; - let (object, object_type) = (&priv_def.object_name, &priv_def.object_type); + let (object, object_type) = (priv_def.object_id(), &priv_def.object_type); let (grantee_type, grantee) = priv_def.grantee_type_and_name(storage)?; match (privilege.as_str(), object_type.as_str()) { ("execute", "role") => { + let object = object.expect("should be set"); crate::audit!( message: "revoke role `{object}` from {grantee_type} `{grantee}`", title: "revoke_role", @@ -2822,13 +2816,17 @@ pub mod acl { ); } _ => { + let object = match object { + Some(object) => format!("`{object}` "), + None => "".into(), + }; crate::audit!( - message: "revoked privilege {privilege} on {object_type} `{object}` \ + message: "revoked privilege {privilege} on {object_type} {object}\ from {grantee_type} `{grantee}`", title: "revoke_privilege", severity: High, privilege: privilege, - object: object, + object: priv_def.object_id(), object_type: object_type, grantee: &grantee, grantee_type: grantee_type, @@ -2939,18 +2937,18 @@ pub mod acl { pub fn on_master_grant_privilege(priv_def: &PrivilegeDef) -> tarantool::Result<()> { let lua = ::tarantool::lua_state(); lua.exec_with( - "local grantee_id, privilege, object_type, object_name = ... + "local grantee_id, privilege, object_type, object_id = ... local grantee_def = box.space._user:get(grantee_id) if grantee_def.type == 'user' then - box.schema.user.grant(grantee_id, privilege, object_type, object_name) + box.schema.user.grant(grantee_id, privilege, object_type, object_id) else - box.schema.role.grant(grantee_id, privilege, object_type, object_name) + box.schema.role.grant(grantee_id, privilege, object_type, object_id) end", ( priv_def.grantee_id, &priv_def.privilege, &priv_def.object_type, - &priv_def.object_name, + priv_def.object_id(), ), ) .map_err(LuaError::from)?; @@ -2962,22 +2960,22 @@ pub mod acl { pub fn on_master_revoke_privilege(priv_def: &PrivilegeDef) -> tarantool::Result<()> { let lua = ::tarantool::lua_state(); lua.exec_with( - "local grantee_id, privilege, object_type, object_name = ... + "local grantee_id, privilege, object_type, object_id = ... local grantee_def = box.space._user:get(grantee_id) if not grantee_def then -- Grantee already dropped -> privileges already revoked return end if grantee_def.type == 'user' then - box.schema.user.revoke(grantee_id, privilege, object_type, object_name) + box.schema.user.revoke(grantee_id, privilege, object_type, object_id) else - box.schema.role.revoke(grantee_id, privilege, object_type, object_name) + box.schema.role.revoke(grantee_id, privilege, object_type, object_id) end", ( priv_def.grantee_id, &priv_def.privilege, &priv_def.object_type, - &priv_def.object_name, + priv_def.object_id(), ), ) .map_err(LuaError::from)?; diff --git a/src/traft/op.rs b/src/traft/op.rs index 062ed4e1d3..6472996f29 100644 --- a/src/traft/op.rs +++ b/src/traft/op.rs @@ -152,28 +152,28 @@ impl std::fmt::Display for Op { write!(f, "DropRole({schema_version}, {role_id})") } Self::Acl(Acl::GrantPrivilege { priv_def }) => { + let object_id = priv_def.object_id(); let PrivilegeDef { grantee_id, grantor_id, object_type, - object_name, privilege, schema_version, .. } = priv_def; - write!(f, "GrantPrivilege({schema_version}, {grantor_id}, {grantee_id}, {object_type}, {object_name}, {privilege})") + write!(f, "GrantPrivilege({schema_version}, {grantor_id}, {grantee_id}, {object_type}, {object_id:?}, {privilege})") } Self::Acl(Acl::RevokePrivilege { priv_def }) => { + let object_id = priv_def.object_id(); let PrivilegeDef { grantee_id, grantor_id, object_type, - object_name, privilege, schema_version, .. } = priv_def; - write!(f, "RevokePrivilege({schema_version}, {grantor_id}, {grantee_id}, {object_type}, {object_name}, {privilege})") + write!(f, "RevokePrivilege({schema_version}, {grantor_id}, {grantee_id}, {object_type}, {object_id:?}, {privilege})") } }; diff --git a/test/int/test_acl.py b/test/int/test_acl.py index 4ab169a52b..aee5303c66 100644 --- a/test/int/test_acl.py +++ b/test/int/test_acl.py @@ -252,11 +252,30 @@ def test_acl_lua_api(cluster: Cluster): dave_id = i1.call("box.space._pico_user.index.name:get", "Dave")[0] + pico_property_id = i1.eval("return box.space._pico_property.id") priv = i1.call( - "box.space._pico_privilege:get", (dave_id, "space", "_pico_property", "read") + "box.space._pico_privilege:get", + (dave_id, "space", pico_property_id, "read"), ) - assert priv[0] == 0 # The above grant was executed from guest. 0 is guest user id. + # grantor_id is the field at index 4 in schema::PrivilegeDef + assert priv[4] == 0 # The above grant was executed from guest. 0 is guest user id. + + # Grant privilege to user without object_name -> Ok. + i1.call( + "pico.grant_privilege", + "Dave", + "create", + "user", + ) + + priv = i1.call( + "box.space._pico_privilege:get", + (dave_id, "user", -1, "create"), + ) + + # grantor_id is the field at index 4 in schema::PrivilegeDef + assert priv[4] == 0 # The above grant was executed from guest. 0 is guest user id. # Already granted -> ok. i1.call( @@ -295,7 +314,7 @@ def test_acl_lua_api(cluster: Cluster): # pico.revoke_privilege semantics verification # - # Revoke privilege to user -> Ok. + # Revoke privilege from user -> Ok. i1.call( "pico.revoke_privilege", "Dave", @@ -304,6 +323,14 @@ def test_acl_lua_api(cluster: Cluster): "_pico_property", ) + # Revoke privilege from user without object_name -> Ok. + i1.call( + "pico.revoke_privilege", + "Dave", + "create", + "user", + ) + # Already revoked -> ok. i1.call( "pico.revoke_privilege", @@ -313,7 +340,7 @@ def test_acl_lua_api(cluster: Cluster): "_pico_property", ) - # Revoke privilege to role -> Ok. + # Revoke privilege from role -> Ok. i1.call( "pico.revoke_privilege", "Parent", @@ -331,7 +358,7 @@ def test_acl_lua_api(cluster: Cluster): "_pico_property", ) - # Revoke role to user -> Ok. + # Revoke role from user -> Ok. i1.call("pico.revoke_privilege", "Dave", "execute", "role", "Parent") # Already revoked role to user -> ok. @@ -862,6 +889,7 @@ def test_builtin_users_and_roles(cluster: Cluster): "_pico_property", ) + # granting already granted privilege does not raise an error index = i1.call("pico.raft_get_index") new_index = i1.call( "pico.grant_privilege", @@ -869,7 +897,6 @@ def test_builtin_users_and_roles(cluster: Cluster): "write", "universe", ) - assert index == new_index index = i1.call("pico.create_user", "Dave", VALID_PASSWORD) diff --git a/test/int/test_basics.py b/test/int/test_basics.py index 8f311c536c..d84c7bb7f9 100644 --- a/test/int/test_basics.py +++ b/test/int/test_basics.py @@ -255,30 +255,30 @@ def test_raft_log(instance: Instance): | 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])| +| 16 | 1 |1.0.16|Insert({_pico_privilege}, ["usage","universe",0,0,1,0])| +| 17 | 1 |1.0.17|Insert({_pico_privilege}, ["session","universe",0,0,1,0])| +| 18 | 1 |1.0.18|Insert({_pico_privilege}, ["execute","role",2,0,1,0])| +| 19 | 1 |1.0.19|Insert({_pico_privilege}, ["execute","role",31,0,1,0])| +| 20 | 1 |1.0.20|Insert({_pico_privilege}, ["read","universe",0,1,1,0])| +| 21 | 1 |1.0.21|Insert({_pico_privilege}, ["write","universe",0,1,1,0])| +| 22 | 1 |1.0.22|Insert({_pico_privilege}, ["execute","universe",0,1,1,0])| +| 23 | 1 |1.0.23|Insert({_pico_privilege}, ["session","universe",0,1,1,0])| +| 24 | 1 |1.0.24|Insert({_pico_privilege}, ["usage","universe",0,1,1,0])| +| 25 | 1 |1.0.25|Insert({_pico_privilege}, ["create","universe",0,1,1,0])| +| 26 | 1 |1.0.26|Insert({_pico_privilege}, ["drop","universe",0,1,1,0])| +| 27 | 1 |1.0.27|Insert({_pico_privilege}, ["alter","universe",0,1,1,0])| +| 28 | 1 |1.0.28|Insert({_pico_privilege}, ["grant","universe",0,1,1,0])| +| 29 | 1 |1.0.29|Insert({_pico_privilege}, ["revoke","universe",0,1,1,0])| +| 30 | 1 |1.0.30|Insert({_pico_privilege}, ["read","universe",0,31,1,0])| +| 31 | 1 |1.0.31|Insert({_pico_privilege}, ["write","universe",0,31,1,0])| +| 32 | 1 |1.0.32|Insert({_pico_privilege}, ["execute","universe",0,31,1,0])| +| 33 | 1 |1.0.33|Insert({_pico_privilege}, ["session","universe",0,31,1,0])| +| 34 | 1 |1.0.34|Insert({_pico_privilege}, ["usage","universe",0,31,1,0])| +| 35 | 1 |1.0.35|Insert({_pico_privilege}, ["create","universe",0,31,1,0])| +| 36 | 1 |1.0.36|Insert({_pico_privilege}, ["drop","universe",0,31,1,0])| +| 37 | 1 |1.0.37|Insert({_pico_privilege}, ["alter","universe",0,31,1,0])| +| 38 | 1 |1.0.38|Insert({_pico_privilege}, ["grant","universe",0,31,1,0])| +| 39 | 1 |1.0.39|Insert({_pico_privilege}, ["revoke","universe",0,31,1,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"])| -- GitLab