diff --git a/src/access_control.rs b/src/access_control.rs
index 374f89b13101b7271410b08047cb1876d7c3dccb..b009a680f13e2f8a7ba3f3d9ad06055e6bef43b5 100644
--- a/src/access_control.rs
+++ b/src/access_control.rs
@@ -532,7 +532,8 @@ mod tests {
             grantee_id,
             grantor_id.unwrap_or(session::uid().unwrap()),
             0,
-        );
+        )
+        .expect("must be valid");
 
         access_check_op(
             &Op::Acl(Acl::GrantPrivilege {
@@ -559,7 +560,8 @@ mod tests {
             grantee_id,
             session::uid().unwrap(),
             0,
-        );
+        )
+        .expect("must be valid");
 
         access_check_op(
             &Op::Acl(Acl::RevokePrivilege {
diff --git a/src/schema.rs b/src/schema.rs
index a841dabed7e6db2eeb142358f6830cd26e96ef61..7988c8aff51900e2be33ae7ba41e89306ee408bd 100644
--- a/src/schema.rs
+++ b/src/schema.rs
@@ -1,6 +1,7 @@
 use once_cell::sync::OnceCell;
 use std::borrow::Cow;
 use std::collections::{BTreeMap, HashSet};
+use std::fmt::Display;
 use std::time::Duration;
 
 use tarantool::auth::AuthDef;
@@ -379,14 +380,20 @@ tarantool::define_str_enum! {
         Drop = "drop",
         /// ALTER
         Alter = "alter",
-        /// This is never granted, but used internally.
-        Grant = "grant",
-        /// Never granted, but used internally.
-        Revoke = "revoke",
     }
 }
 
 /// Privilege definition.
+/// Note the differences between picodata privileges and vanilla tarantool ones.
+/// 1) Super role in picodata is a placeholder. It exists because picodata reuses user
+///    ids from vanilla and super occupies an id. So to avoid assigning some piciodata
+///    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.
+/// 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.
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
 pub struct PrivilegeDef {
     privilege: PrivilegeType,
@@ -410,6 +417,22 @@ pub struct PrivilegeDef {
 
 impl Encode for PrivilegeDef {}
 
+#[derive(Debug, thiserror::Error, PartialEq, Eq)]
+pub struct InvalidPrivilegeError {
+    object_type: SchemaObjectType,
+    unsupported: PrivilegeType,
+    expected_one_of: &'static [PrivilegeType],
+}
+
+impl Display for InvalidPrivilegeError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_fmt(format_args!(
+            "Unsupported {} privilege '{}', expected one of {:?}",
+            self.object_type, self.unsupported, self.expected_one_of,
+        ))
+    }
+}
+
 impl PrivilegeDef {
     pub fn new(
         privilege: PrivilegeType,
@@ -418,15 +441,43 @@ impl PrivilegeDef {
         grantee_id: UserId,
         grantor_id: UserId,
         schema_version: u64,
-    ) -> PrivilegeDef {
-        PrivilegeDef {
+    ) -> Result<PrivilegeDef, InvalidPrivilegeError> {
+        let privilege_def = PrivilegeDef {
             privilege,
             object_type,
             object_id,
             grantee_id,
             grantor_id,
             schema_version,
+        };
+
+        use PrivilegeType::*;
+
+        let valid_privileges: &[PrivilegeType] = match privilege_def.object_type {
+            SchemaObjectType::Table => match privilege_def.object_id() {
+                Some(_) => &[Read, Write, Alter, Drop],
+                None => &[Read, Write, Create, Alter, Drop],
+            },
+            SchemaObjectType::Role => match privilege_def.object_id() {
+                Some(_) => &[Execute, Drop],
+                None => &[Create, Drop],
+            },
+            SchemaObjectType::User => match privilege_def.object_id() {
+                Some(_) => &[Alter, Drop],
+                None => &[Create, Alter, Drop],
+            },
+            SchemaObjectType::Universe => &[Session, Usage],
+        };
+
+        if !valid_privileges.contains(&privilege) {
+            return Err(InvalidPrivilegeError {
+                object_type,
+                unsupported: privilege,
+                expected_one_of: valid_privileges,
+            });
         }
+
+        Ok(privilege_def)
     }
 
     #[inline(always)]
@@ -503,15 +554,18 @@ impl PrivilegeDef {
             let mut v = Vec::new();
 
             // SQL: GRANT <'usage', 'session'> ON 'universe' TO 'guest'
-            for privilege in [PrivilegeType::Usage, PrivilegeType::Session] {
-                v.push(PrivilegeDef {
-                    grantor_id: ADMIN_ID,
-                    grantee_id: GUEST_ID,
-                    object_type: SchemaObjectType::Universe,
-                    object_id: UNIVERSE_ID,
-                    privilege,
-                    schema_version: 0,
-                });
+            // SQL: GRANT <'usage', 'session'> ON 'universe' TO 'admin'
+            for user in [GUEST_ID, ADMIN_ID] {
+                for privilege in [PrivilegeType::Usage, PrivilegeType::Session] {
+                    v.push(PrivilegeDef {
+                        grantor_id: ADMIN_ID,
+                        grantee_id: user,
+                        object_type: SchemaObjectType::Universe,
+                        object_id: UNIVERSE_ID,
+                        privilege,
+                        schema_version: 0,
+                    });
+                }
             }
 
             // SQL: GRANT 'public' TO 'guest'
@@ -524,18 +578,6 @@ impl PrivilegeDef {
                 schema_version: 0,
             });
 
-            // SQL: GRANT 'all privileges' ON 'universe' TO 'admin'
-            for privilege in PrivilegeType::VARIANTS {
-                v.push(PrivilegeDef {
-                    grantor_id: ADMIN_ID,
-                    grantee_id: ADMIN_ID,
-                    object_type: SchemaObjectType::Universe,
-                    object_id: UNIVERSE_ID,
-                    privilege: *privilege,
-                    schema_version: 0,
-                });
-            }
-
             v
         })
     }
@@ -1266,7 +1308,7 @@ mod tests {
 #[cfg(test)]
 mod test {
     use super::*;
-    use tarantool::tuple::ToTupleBuffer;
+    use tarantool::tuple::{Decode, ToTupleBuffer};
 
     #[test]
     #[rustfmt::skip]
@@ -1318,4 +1360,61 @@ mod test {
         let format = PrivilegeDef::format();
         crate::util::check_tuple_matches_format(tuple_data.as_ref(), &format, "PrivilegeDef::format");
     }
+
+    #[track_caller]
+    fn check_object_privilege(
+        object_type: SchemaObjectType,
+        valid_privileges: &'static [PrivilegeType],
+        object_id: i64,
+    ) {
+        for privilege in PrivilegeType::VARIANTS {
+            let res = PrivilegeDef::new(*privilege, object_type, object_id, 1, 1, 1);
+
+            if valid_privileges.contains(privilege) {
+                assert!(res.is_ok())
+            } else {
+                assert_eq!(
+                    res.unwrap_err(),
+                    InvalidPrivilegeError {
+                        object_type,
+                        unsupported: *privilege,
+                        expected_one_of: valid_privileges,
+                    }
+                )
+            }
+        }
+    }
+
+    #[test]
+    fn privilege_def_validation() {
+        use PrivilegeType::*;
+
+        // table
+        let valid = &[Read, Write, Create, Alter, Drop];
+        check_object_privilege(SchemaObjectType::Table, valid, -1);
+
+        // particular table
+        let valid = &[Read, Write, Alter, Drop];
+        check_object_privilege(SchemaObjectType::Table, valid, 42);
+
+        // user
+        let valid = &[Create, Alter, Drop];
+        check_object_privilege(SchemaObjectType::User, valid, -1);
+
+        // particular user
+        let valid = &[Alter, Drop];
+        check_object_privilege(SchemaObjectType::User, valid, 42);
+
+        // role
+        let valid = &[Create, Drop];
+        check_object_privilege(SchemaObjectType::Role, valid, -1);
+
+        // particular role
+        let valid = &[Execute, Drop];
+        check_object_privilege(SchemaObjectType::Role, valid, 42);
+
+        // universe
+        let valid = &[Session, Usage];
+        check_object_privilege(SchemaObjectType::Universe, valid, 0);
+    }
 }
diff --git a/src/sql.rs b/src/sql.rs
index 407152b392758a63ec7813ffdbb4d2e6f1f5ce3f..764136a8a1103aeb0e42f68d0f6033cb993f890d 100644
--- a/src/sql.rs
+++ b/src/sql.rs
@@ -800,7 +800,8 @@ fn reenterable_schema_change_request(
                     grantee_id,
                     grantor_id,
                     schema_version,
-                );
+                )
+                .map_err(Error::other)?;
 
                 match alter_option_param {
                     AlterOptionParam::ChangePassword(auth) => {
@@ -912,7 +913,8 @@ fn reenterable_schema_change_request(
                         grantee_id,
                         grantor_id,
                         schema_version,
-                    ),
+                    )
+                    .map_err(Error::other)?,
                 })
             }
             Params::RevokePrivilege(revoke_type, grantee_name) => {
@@ -939,7 +941,8 @@ fn reenterable_schema_change_request(
                         grantee_id,
                         grantor_id,
                         schema_version,
-                    ),
+                    )
+                    .map_err(Error::other)?,
                 })
             }
         };
diff --git a/src/storage.rs b/src/storage.rs
index fb89dbade15e7944da28d48e2e673bace1c0be88..823d43fca20f025523a89ed88b7bc8e7f76a81e5 100644
--- a/src/storage.rs
+++ b/src/storage.rs
@@ -2477,6 +2477,7 @@ impl Privileges {
     /// Remove any privilege definitions assigning the given role.
     #[inline]
     pub fn delete_all_by_granted_role(&self, role_id: u32) -> Result<()> {
+        // here we failed to deserialize the privilege that has alter on universe
         for priv_def in self.iter()? {
             if priv_def.privilege() == PrivilegeType::Execute
                 && priv_def.object_type() == SchemaObjectType::Role
@@ -2730,9 +2731,7 @@ impl SchemaDef for PrivilegeDef {
 
     #[inline(always)]
     fn schema_version(&self) -> u64 {
-        // Note: this doesnt lead to recursion because of the
-        // method resolution order: https://doc.rust-lang.org/reference/expressions/method-call-expr.html
-        self.schema_version()
+        PrivilegeDef::schema_version(self)
     }
 
     #[inline(always)]
diff --git a/test/conftest.py b/test/conftest.py
index 6fd0c643828b04046cf76ff3e0e25754b0d8df9a..0402a2f72652e1bca0bd608c9869463efb24fe30 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -1354,6 +1354,32 @@ class Cluster:
         eprint(f"CaS:\n  {predicate=}\n  {dml=}")
         return instance.call("pico.cas", dml, predicate, user=user, password=password)
 
+    def masters(self) -> List[Instance]:
+        ret = []
+        for instance in self.instances:
+            if not instance.eval("return box.info.ro"):
+                ret.append(instance)
+
+        return ret
+
+    def grant_box_privilege(
+        self, user, privilege: str, object_type: str, object_name: Optional[str] = None
+    ):
+        """
+        Sometimes in our tests we go beyond picodata privilege model and need
+        to grant priveleges on something that is not part of the picodata access control model.
+        For example execute access on universe mainly needed to invoke functions.
+        """
+        for instance in self.masters():
+            instance.eval(
+                """
+                box.session.su("admin")
+                user, privilege, object_type, object_name = ...
+                return box.schema.user.grant(user, privilege, object_type, object_name)
+                """,
+                [user, privilege, object_type, object_name],
+            )
+
 
 @dataclass
 class PortalStorage:
diff --git a/test/int/test_acl.py b/test/int/test_acl.py
index 3c80452266411dd14bc0ab784f1ad05e32f5bf11..4279464498c159c2180415450b7f6b6784d2746e 100644
--- a/test/int/test_acl.py
+++ b/test/int/test_acl.py
@@ -202,9 +202,9 @@ def test_acl_lua_api(cluster: Cluster):
             i1.call(
                 f"pico.{f}",
                 "User is not found",
-                "execute",
-                "universe",
-                None,
+                "read",
+                "table",
+                "_pico_property",
             )
 
         # No such privilege -> error.
@@ -219,7 +219,7 @@ def test_acl_lua_api(cluster: Cluster):
             ReturnError,
             match=rf"unsupported privilege 'read,write', see pico.help\('{f}'\) for details",
         ):
-            i1.call(f"pico.{f}", "Dave", "read,write", "universe", None)
+            i1.call(f"pico.{f}", "Dave", "read,write", "role", None)
 
         # No object_type -> error.
         with pytest.raises(ReturnError, match="object_type should be a string"):
@@ -463,9 +463,7 @@ def test_acl_basic(cluster: Cluster):
     # Grant some privileges.
     # Doing anything via remote function execution requires execute access
     # to the "universe"
-    index = i1.grant_privilege(user, "execute", "universe")
-    cluster.raft_wait_index(index)
-    v += 1
+    cluster.grant_box_privilege(user, "execute", "universe")
 
     index = i1.grant_privilege(user, "read", "table", "money")
     cluster.raft_wait_index(index)
@@ -592,8 +590,7 @@ def test_acl_roles_basic(cluster: Cluster):
 
     # Doing anything via remote function execution requires execute access
     # to the "universe"
-    index = i1.grant_privilege(user, "execute", "universe", None)
-    cluster.raft_wait_index(index)
+    cluster.grant_box_privilege(user, "execute", "universe", None)
 
     # Try reading from table on behalf of the user.
     for i in cluster.instances:
@@ -757,8 +754,8 @@ def test_acl_from_snapshot(cluster: Cluster):
             ("read", "space", "_pico_table"),
         }
 
-        with pytest.raises(TarantoolError, match="Role 'Executor' is not found"):
-            i.call("box.schema.role.info", "Executor")
+        with pytest.raises(TarantoolError, match="Role 'Writer' is not found"):
+            i.call("box.schema.role.info", "Writer")
 
     #
     # These will be catching up by snapshot.
@@ -791,13 +788,13 @@ def test_acl_from_snapshot(cluster: Cluster):
     )
     cluster.raft_wait_index(index)
 
-    index = i1.call("pico.create_role", "Executor")
+    index = i1.call("pico.create_role", "Writer")
     cluster.raft_wait_index(index)
 
-    index = i1.grant_privilege("Executor", "execute", "universe", None)
+    index = i1.grant_privilege("Writer", "write", "table", None)
     cluster.raft_wait_index(index)
 
-    index = i1.grant_privilege("Blam", "execute", "role", "Executor")
+    index = i1.grant_privilege("Blam", "execute", "role", "Writer")
     cluster.raft_wait_index(index)
 
     # Compact log to trigger snapshot generation.
@@ -822,7 +819,7 @@ def test_acl_from_snapshot(cluster: Cluster):
 
         assert to_set_of_tuples(i.call("box.schema.user.info", "Blam")) == {
             ("execute", "role", "public"),
-            ("execute", "role", "Executor"),
+            ("execute", "role", "Writer"),
             ("session,usage", "universe", ""),
             ("alter", "user", "Blam"),
         }
@@ -831,8 +828,8 @@ def test_acl_from_snapshot(cluster: Cluster):
             ("read", "space", "_pico_instance"),
         }
 
-        assert to_set_of_tuples(i.call("box.schema.role.info", "Executor")) == {
-            ("execute", "universe", ""),
+        assert to_set_of_tuples(i.call("box.schema.role.info", "Writer")) == {
+            ("write", "space", ""),
         }
 
 
@@ -886,16 +883,18 @@ def test_builtin_users_and_roles(cluster: Cluster):
             "_pico_property",
         )
 
+    index = i1.call("pico.create_user", "Dave", VALID_PASSWORD)
+    i1.raft_wait_index(index)
+
     # granting already granted privilege does not raise an error
     index = i1.call("pico.raft_get_index")
     new_index = i1.grant_privilege(
-        "admin",
-        "write",
+        "Dave",
+        "session",
         "universe",
     )
     assert index == new_index
 
-    index = i1.call("pico.create_user", "Dave", VALID_PASSWORD)
     new_index = i1.grant_privilege(
         "Dave",
         "execute",
diff --git a/test/int/test_basics.py b/test/int/test_basics.py
index 5516edd052b4575385775456a30211b5f89231ec..e3011a8c5cc2997343bce6601a2d655313f151aa 100644
--- a/test/int/test_basics.py
+++ b/test/int/test_basics.py
@@ -257,27 +257,19 @@ def test_raft_log(instance: Instance):
 | 15  | 1  |1.0.15|Insert({_pico_role}, [31,"super",0,1])|
 | 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}, ["read","universe",0,1,1,0])|
-| 20  | 1  |1.0.20|Insert({_pico_privilege}, ["write","universe",0,1,1,0])|
-| 21  | 1  |1.0.21|Insert({_pico_privilege}, ["execute","universe",0,1,1,0])|
-| 22  | 1  |1.0.22|Insert({_pico_privilege}, ["session","universe",0,1,1,0])|
-| 23  | 1  |1.0.23|Insert({_pico_privilege}, ["usage","universe",0,1,1,0])|
-| 24  | 1  |1.0.24|Insert({_pico_privilege}, ["create","universe",0,1,1,0])|
-| 25  | 1  |1.0.25|Insert({_pico_privilege}, ["drop","universe",0,1,1,0])|
-| 26  | 1  |1.0.26|Insert({_pico_privilege}, ["alter","universe",0,1,1,0])|
-| 27  | 1  |1.0.27|Insert({_pico_privilege}, ["grant","universe",0,1,1,0])|
-| 28  | 1  |1.0.28|Insert({_pico_privilege}, ["revoke","universe",0,1,1,0])|
-| 29  | 1  |     |AddNode(1)|
-| 30  | 2  |     |-|
-| 31  | 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"])|
-| 32  | 2  |1.1.2|Insert({_pico_replicaset}, ["r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07","i1","default",0.0,"auto","not-ready"])|
-| 33  | 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"])|
-| 34  | 2  |1.1.4|Update({_pico_replicaset}, ["r1"], [["=","weight",1.0], ["=","state","ready"]])|
-| 35  | 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"]])|
-| 36  | 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"]])|
-| 37  | 2  |1.1.7|Replace({_pico_property}, ["vshard_bootstrapped",true])|
-| 38  | 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"])|
+| 18  | 1  |1.0.18|Insert({_pico_privilege}, ["usage","universe",0,1,1,0])|
+| 19  | 1  |1.0.19|Insert({_pico_privilege}, ["session","universe",0,1,1,0])|
+| 20  | 1  |1.0.20|Insert({_pico_privilege}, ["execute","role",2,0,1,0])|
+| 21  | 1  |     |AddNode(1)|
+| 22  | 2  |     |-|
+| 23  | 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"])|
+| 24  | 2  |1.1.2|Insert({_pico_replicaset}, ["r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07","i1","default",0.0,"auto","not-ready"])|
+| 25  | 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"])|
+| 26  | 2  |1.1.4|Update({_pico_replicaset}, ["r1"], [["=","weight",1.0], ["=","state","ready"]])|
+| 27  | 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"]])|
+| 28  | 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"]])|
+| 29  | 2  |1.1.7|Replace({_pico_property}, ["vshard_bootstrapped",true])|
+| 30  | 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,
@@ -291,7 +283,7 @@ def test_raft_log(instance: Instance):
         _pico_user=space_id("_pico_user"),
         _pico_role=space_id("_pico_role"),
     )
-    assert strip_spaces(expected) == strip_spaces(raft_log)
+    assert strip_spaces(raft_log) == strip_spaces(expected)
 
 
 def test_governor_notices_restarts(instance: Instance):
diff --git a/test/int/test_sql.py b/test/int/test_sql.py
index 960982c2b609eba67c70588cdb33c406982a1275..528192325c636df74ba753279c3cf328ded4f212 100644
--- a/test/int/test_sql.py
+++ b/test/int/test_sql.py
@@ -936,8 +936,6 @@ def test_sql_alter_login(cluster: Cluster):
     # * TODO: Check SESSION privilege is removed.
 
 
-# TODO: replace all Lua `i1.call`s to SQL `iq.sql`.
-# See https://git.picodata.io/picodata/picodata/sbroad/-/issues/492.
 def test_sql_acl_privileges(cluster: Cluster):
     cluster.deploy(instance_count=2)
     i1, i2 = cluster.instances
@@ -977,9 +975,6 @@ def test_sql_acl_privileges(cluster: Cluster):
     )
     assert ddl["row_count"] == 1
 
-    # Grant remote functions call.
-    i1.grant_privilege(username, "execute", "universe", None)
-
     # Remember number of default privileges.
     default_privileges_number = len(
         i1.sql(""" select * from "_pico_privilege" """)["rows"]