Tarantool development patches archive
 help / color / mirror / Atom feed
* [tarantool-patches] [PATCH 0/4] Fixes in access control and privileges
@ 2018-07-17 15:47 Serge Petrenko
  2018-07-17 15:47 ` [tarantool-patches] [PATCH 1/4] Make access_check_ddl check for entity privileges Serge Petrenko
                   ` (4 more replies)
  0 siblings, 5 replies; 11+ messages in thread
From: Serge Petrenko @ 2018-07-17 15:47 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Serge Petrenko

This patch set fixes various issues with access control,
mostly in function access_check_ddl().

Patches 1-3 already were sent separately a couple of days
ago, but I believe they belong together, since every next
one is based on the previous.
Also I rebased patches 1-3 to the latest 1.10.

Patch 1 adds an entity privilege check to access_check_ddl
https://github.com/tarantool/tarantool/tree/sergepetrenko/gh-3516-entity-access-checks
https://github.com/tarantool/tarantool/issues/3516
Patch 2 is a follow-up to patch 1 and adds ACLs for entities
user and role.
https://github.com/tarantool/tarantool/tree/sergepetrenko/gh-3524-entity-access-grants
https://github.com/tarantool/tarantool/issues/3524
Patch 3 is a follow-up to patch 2 and adds single object access
checks in access_check_ddl and adds ACLs to a single object
user(and role).
https://github.com/tarantool/tarantool/tree/sergepetrenko/gh-3530-object-access-checks
https://github.com/tarantool/tarantool/issues/3530
Patch 4 adds an upgrade script which should fire on update to
1.10 and grant create/alter/drop privileges to users with
read and write access. Also Patch 4 modifies tests to grant
entity and object privileges instead of universal privileges.
This is made possible by patches 1-3.
https://github.com/tarantool/tarantool/tree/sergepetrenko/gh-3539-1.10-upgrade-script
https://github.com/tarantool/tarantool/issues/3539

Serge Petrenko (4):
  Make access_check_ddl check for entity privileges.
  Add entities user, role to access control.
  Add single object privilege checks to access_check_ddl.
  Add a privilege upgrade script and update tests.

 src/box/alter.cc                            | 226 ++++++++++++++++++------
 src/box/lua/schema.lua                      |  30 ++--
 src/box/lua/upgrade.lua                     |  22 +++
 src/box/schema.h                            |   6 +
 src/box/user.cc                             |  31 +++-
 src/box/user.h                              |   2 +
 test/box-tap/auth.test.lua                  |   5 -
 test/box-tap/session.test.lua               |  15 +-
 test/box/access.result                      | 215 +++++++++++++++++++++-
 test/box/access.test.lua                    |  73 +++++++-
 test/box/access_bin.result                  |   4 +-
 test/box/access_bin.test.lua                |   4 +-
 test/box/access_escalation.result           |  15 +-
 test/box/access_escalation.test.lua         |  10 +-
 test/box/access_misc.result                 |   6 +-
 test/box/access_misc.test.lua               |   4 +-
 test/box/call.result                        |   4 +-
 test/box/call.test.lua                      |   4 +-
 test/box/errinj.result                      |  30 +++-
 test/box/errinj.test.lua                    |  21 ++-
 test/box/net.box.result                     | 265 ++++++++++++++++++++++++++--
 test/box/net.box.test.lua                   | 121 +++++++++++--
 test/box/net_msg_max.result                 |  13 +-
 test/box/net_msg_max.test.lua               |   9 +-
 test/box/on_replace.result                  |   2 +-
 test/box/on_replace.test.lua                |   2 +-
 test/box/protocol.result                    |   9 +-
 test/box/protocol.test.lua                  |   5 +-
 test/box/push.result                        |  52 +++++-
 test/box/push.test.lua                      |  27 ++-
 test/box/role.result                        |  34 +++-
 test/box/role.test.lua                      |  12 +-
 test/box/schema_reload.result               |  32 +++-
 test/box/schema_reload.test.lua             |  16 +-
 test/box/sequence.result                    | 131 ++++++++++++--
 test/box/sequence.test.lua                  |  58 ++++--
 test/box/sql.result                         |   9 +-
 test/box/sql.test.lua                       |   5 +-
 test/box/stat_net.result                    |   7 +-
 test/box/stat_net.test.lua                  |   5 +-
 test/engine/params.result                   |   6 -
 test/engine/params.test.lua                 |   2 -
 test/engine/replica_join.result             |   6 -
 test/engine/replica_join.test.lua           |   2 -
 test/replication/autobootstrap.result       |  23 ++-
 test/replication/autobootstrap.test.lua     |  10 +-
 test/replication/catch.result               |   6 -
 test/replication/catch.test.lua             |   2 -
 test/replication/errinj.result              |   3 -
 test/replication/errinj.test.lua            |   1 -
 test/replication/gc.result                  |   6 -
 test/replication/gc.test.lua                |   2 -
 test/replication/join_vclock.result         |   6 -
 test/replication/join_vclock.test.lua       |   2 -
 test/replication/skip_conflict_row.result   |   6 -
 test/replication/skip_conflict_row.test.lua |   2 -
 test/vinyl/replica_quota.result             |   6 -
 test/vinyl/replica_quota.test.lua           |   2 -
 test/wal_off/func_max.result                |  25 +--
 test/wal_off/func_max.test.lua              |  19 +-
 test/xlog/errinj.result                     |   9 +-
 test/xlog/errinj.test.lua                   |   4 +-
 test/xlog/misc.result                       |   9 +-
 test/xlog/misc.test.lua                     |   5 +-
 test/xlog/upgrade.result                    |   4 +-
 65 files changed, 1380 insertions(+), 329 deletions(-)

-- 
2.15.2 (Apple Git-101.1)

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [tarantool-patches] [PATCH 1/4] Make access_check_ddl check for entity privileges.
  2018-07-17 15:47 [tarantool-patches] [PATCH 0/4] Fixes in access control and privileges Serge Petrenko
@ 2018-07-17 15:47 ` Serge Petrenko
  2018-07-17 15:47 ` [tarantool-patches] [PATCH 2/4] Add entities user, role to access control Serge Petrenko
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 11+ messages in thread
From: Serge Petrenko @ 2018-07-17 15:47 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Serge Petrenko

Function access_check_ddl checked only for universal access, thus
granting entity or singe object access to a user would have no effect in
scope of this function.
Fix this by adding entity access checks.

Also attaching an existing sequence to a space checked for
create privilege on both space (instead of alter) and sequence
(instead of read + write). Fixed both and changed the tests accordingly.

Now creating an index demands alter on a space, because checking for
create would lead to anyone with create access to entity space be able
to create indices in other users spaces. Also checking for alter or drop
on dropping an index, since any operations on space indices are altering
a space, and dropping a space requires dropping all indices, so have to
check for alter (in case of just dropping an index) or drop (in case of
dropping the space with all its indices, to not require additional alter
privilege).

Closes #3516
---
 src/box/alter.cc           |  53 +++++++++++++++----
 test/box/access.result     |  29 ++++++++--
 test/box/access.test.lua   |  21 ++++++--
 test/box/sequence.result   | 128 ++++++++++++++++++++++++++++++++++++++++-----
 test/box/sequence.test.lua |  58 +++++++++++++++-----
 5 files changed, 245 insertions(+), 44 deletions(-)

diff --git a/src/box/alter.cc b/src/box/alter.cc
index b74369321..bd50e457a 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -85,6 +85,13 @@ access_check_ddl(const char *name, uint32_t owner_uid,
 	user_access_t access = ((PRIV_U | (user_access_t) priv_type) &
 				~has_access);
 	bool is_owner = owner_uid == cr->uid || cr->uid == ADMIN;
+	if (access == 0)
+		return; /* Access granted. */
+	/* Check for specific entity access. */
+	struct access *object = entity_access_get(type);
+	if (object) {
+		access &= ~object[cr->auth_token].effective;
+	}
 	/*
 	 * Only the owner of the object or someone who has
 	 * specific DDL privilege on the object can execute
@@ -94,7 +101,7 @@ access_check_ddl(const char *name, uint32_t owner_uid,
 	 * the owner of the object, but this should be ignored --
 	 * CREATE privilege is required.
 	 */
-	if (access == 0 || (is_owner && !(access & (PRIV_U|PRIV_C))))
+	if (access == 0 || (is_owner && !(access & (PRIV_U | PRIV_C))))
 		return; /* Access granted. */
 
 	/* Create a meaningful error message. */
@@ -1764,11 +1771,20 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
 	uint32_t iid = tuple_field_u32_xc(old_tuple ? old_tuple : new_tuple,
 					  BOX_INDEX_FIELD_ID);
 	struct space *old_space = space_cache_find_xc(id);
-	enum priv_type priv_type = new_tuple ? PRIV_C : PRIV_D;
-	if (old_tuple && new_tuple)
-		priv_type = PRIV_A;
-	access_check_ddl(old_space->def->name, old_space->def->uid, SC_SPACE,
-			 priv_type, true);
+	bool have_alter = true;
+	try {
+		access_check_ddl(old_space->def->name, old_space->def->uid, SC_SPACE,
+				 PRIV_A, true);
+	} catch(AccessDeniedError *e) {
+		/*
+		 * We need alter access on space in case of creating or
+		 * altering an index. But we can have alter OR drop on
+		 * space in case of dropping an index.
+		 */
+		have_alter = false;
+		if(new_tuple)
+			throw;
+	}
 	struct index *old_index = space_index(old_space, iid);
 
 	/*
@@ -1826,6 +1842,13 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
 	 */
 	/* Case 1: drop the index, if it is dropped. */
 	if (old_index != NULL && new_tuple == NULL) {
+		/*
+		 * In case we don't have alter on space when
+		 * dropping an index, check for drop on space.
+		 */
+		if (!have_alter)
+		    access_check_ddl(old_space->def->name, old_space->def->uid,
+				     SC_SPACE, PRIV_D, true);
 		alter_space_move_indexes(alter, 0, iid);
 		(void) new DropIndex(alter, old_index->def);
 	}
@@ -3032,7 +3055,7 @@ on_replace_dd_sequence(struct trigger * /* trigger */, void *event)
 						      ER_CREATE_SEQUENCE);
 		assert(sequence_by_id(new_def->id) == NULL);
 		access_check_ddl(new_def->name, new_def->uid, SC_SEQUENCE,
-			PRIV_C, false);
+				 PRIV_C, false);
 		sequence_cache_replace(new_def);
 		alter->new_def = new_def;
 	} else if (old_tuple != NULL && new_tuple == NULL) {	/* DELETE */
@@ -3135,10 +3158,20 @@ on_replace_dd_space_sequence(struct trigger * /* trigger */, void *event)
 	enum priv_type priv_type = stmt->new_tuple ? PRIV_C : PRIV_D;
 	if (stmt->new_tuple && stmt->old_tuple)
 		priv_type = PRIV_A;
-
 	/* Check we have the correct access type on the sequence.  * */
-	access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE, priv_type,
-			 false);
+	if (is_generated || !stmt->new_tuple) {
+		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
+				 priv_type, false);
+	} else {
+		/*
+		 * In case user wants to attach an existing sequence,
+		 * check that it has read and write access.
+		 */
+		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
+				 PRIV_R, false);
+		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
+				 PRIV_W, false);
+	}
 	/** Check we have alter access on space. */
 	access_check_ddl(space->def->name, space->def->uid, SC_SPACE, PRIV_A,
 			 false);
diff --git a/test/box/access.result b/test/box/access.result
index a1f3e996a..f4669a4a3 100644
--- a/test/box/access.result
+++ b/test/box/access.result
@@ -875,7 +875,12 @@ session = box.session
 box.schema.user.create('test')
 ---
 ...
-box.schema.user.grant('test', 'read,write,create', 'universe')
+box.schema.user.grant('test', 'read', 'space', '_collation')
+---
+...
+--box.schema.user.grant('test', 'write', 'space', '_collation')
+-- FIXME: granting create on 'collation' only doesn't work
+box.schema.user.grant('test', 'create', 'universe')
 ---
 ...
 session.su('test')
@@ -1389,7 +1394,10 @@ box.schema.func.create('test_func')
 box.session.su("admin")
 ---
 ...
-box.schema.user.grant("tester", "read", "universe")
+box.schema.user.grant("tester", "read", "space", "_user")
+---
+...
+box.schema.user.grant("tester", "read", "space", "_func")
 ---
 ...
 -- failed create
@@ -1416,7 +1424,20 @@ box.session.su("admin")
 -- explicitly since we still use process_rw to write to system
 -- tables from ddl
 --
-box.schema.user.grant("tester", "create,write", "universe")
+box.schema.user.grant('tester', 'write', 'universe')
+---
+...
+-- no entity user currently, so have to grant create
+-- on universe in order to create a user.
+box.schema.user.grant('tester', 'create', 'universe')
+---
+...
+-- this should work instead:
+--box.schema.user.grant('tester', 'create', 'user')
+--box.schema.user.grant('tester', 'create', 'space')
+--box.schema.user.grant('tester', 'create', 'function')
+--box.schema.user.grant('tester', 'create' , 'sequence')
+box.schema.user.grant('tester', 'read', 'space', '_sequence')
 ---
 ...
 box.session.su("tester")
@@ -1824,7 +1845,7 @@ _  = box.schema.sequence.create('test_sequence')
 box.session.su('admin')
 ---
 ...
-box.schema.user.grant('tester', 'create', 'universe')
+box.schema.user.grant('tester', 'create', 'sequence')
 ---
 ...
 box.session.su('tester')
diff --git a/test/box/access.test.lua b/test/box/access.test.lua
index fb8f744e8..9ae0e1114 100644
--- a/test/box/access.test.lua
+++ b/test/box/access.test.lua
@@ -340,7 +340,10 @@ c:close()
 
 session = box.session
 box.schema.user.create('test')
-box.schema.user.grant('test', 'read,write,create', 'universe')
+box.schema.user.grant('test', 'read', 'space', '_collation')
+--box.schema.user.grant('test', 'write', 'space', '_collation')
+-- FIXME: granting create on 'collation' only doesn't work
+box.schema.user.grant('test', 'create', 'universe')
 session.su('test')
 box.internal.collation.create('test', 'ICU', 'ru_RU')
 session.su('admin')
@@ -520,7 +523,8 @@ box.schema.space.create("test_space")
 box.schema.user.create('test_user')
 box.schema.func.create('test_func')
 box.session.su("admin")
-box.schema.user.grant("tester", "read", "universe")
+box.schema.user.grant("tester", "read", "space", "_user")
+box.schema.user.grant("tester", "read", "space", "_func")
 -- failed create
 box.session.su("tester")
 box.schema.space.create("test_space")
@@ -533,7 +537,16 @@ box.session.su("admin")
 -- explicitly since we still use process_rw to write to system
 -- tables from ddl
 --
-box.schema.user.grant("tester", "create,write", "universe")
+box.schema.user.grant('tester', 'write', 'universe')
+-- no entity user currently, so have to grant create
+-- on universe in order to create a user.
+box.schema.user.grant('tester', 'create', 'universe')
+-- this should work instead:
+--box.schema.user.grant('tester', 'create', 'user')
+--box.schema.user.grant('tester', 'create', 'space')
+--box.schema.user.grant('tester', 'create', 'function')
+--box.schema.user.grant('tester', 'create' , 'sequence')
+box.schema.user.grant('tester', 'read', 'space', '_sequence')
 box.session.su("tester")
 -- successful create
 s1 = box.schema.space.create("test_space")
@@ -712,7 +725,7 @@ box.schema.user.grant('tester', 'read,write', 'space', '_sequence')
 box.session.su('tester')
 _  = box.schema.sequence.create('test_sequence')
 box.session.su('admin')
-box.schema.user.grant('tester', 'create', 'universe')
+box.schema.user.grant('tester', 'create', 'sequence')
 box.session.su('tester')
 _ = box.schema.sequence.create('test_sequence')
 box.session.su('admin')
diff --git a/test/box/sequence.result b/test/box/sequence.result
index cbbd45080..75d5ea1e6 100644
--- a/test/box/sequence.result
+++ b/test/box/sequence.result
@@ -1471,8 +1471,17 @@ box.session.su('admin')
 sq:drop()
 ---
 ...
+box.schema.user.revoke('user', 'read,write', 'universe')
+---
+...
 -- A user can alter/use sequences that he owns.
-box.schema.user.grant('user', 'create', 'universe')
+box.schema.user.grant('user', 'create', 'sequence')
+---
+...
+box.schema.user.grant('user', 'write', 'space', '_sequence')
+---
+...
+box.schema.user.grant('user', 'write', 'space', '_sequence_data')
 ---
 ...
 box.session.su('user')
@@ -1493,7 +1502,13 @@ sq = box.schema.sequence.create('seq')
 box.session.su('admin')
 ---
 ...
-box.schema.user.revoke('user', 'read,write,create', 'universe')
+box.schema.user.revoke('user', 'create', 'sequence')
+---
+...
+box.schema.user.revoke('user', 'write', 'space', '_sequence')
+---
+...
+box.schema.user.revoke('user', 'write', 'space', '_sequence_data')
 ---
 ...
 box.session.su('user')
@@ -1515,7 +1530,8 @@ box.session.su('admin')
 sq:drop()
 ---
 ...
--- A sequence can be attached to a space only if the user owns both.
+-- A sequence can be attached to a space only if the user has
+-- alter privilege on space and read/write on sequence.
 sq1 = box.schema.sequence.create('seq1')
 ---
 ...
@@ -1525,10 +1541,22 @@ s1 = box.schema.space.create('space1')
 _ = s1:create_index('pk')
 ---
 ...
-box.schema.user.grant('user', 'read,write', 'universe')
+box.schema.user.grant('user', 'write', 'space', '_sequence')
 ---
 ...
-box.schema.user.grant('user', 'create', 'universe')
+box.schema.user.grant('user', 'write', 'space', '_sequence_data')
+---
+...
+box.schema.user.grant('user', 'write', 'space', '_schema')
+---
+...
+box.schema.user.grant('user', 'write', 'space', '_space')
+---
+...
+box.schema.user.grant('user', 'create', 'space')
+---
+...
+box.schema.user.grant('user', 'create', 'sequence')
 ---
 ...
 box.session.su('user')
@@ -1540,17 +1568,53 @@ sq2 = box.schema.sequence.create('seq2')
 s2 = box.schema.space.create('space2')
 ---
 ...
--- fixme: no error on using another user's sequence
-_ = s2:create_index('pk', {sequence = 'seq1'})
+box.session.su('admin')
+---
+...
+box.schema.user.revoke('user', 'create', 'space')
+---
+...
+box.schema.user.revoke('user', 'create', 'sequence')
+---
+...
+box.schema.user.revoke('user', 'write', 'space', '_sequence')
+---
+...
+box.schema.user.revoke('user', 'write', 'space', '_sequence_data')
+---
+...
+box.schema.user.revoke('user', 'write', 'space', '_schema')
+---
+...
+box.schema.user.revoke('user', 'write', 'space', '_space')
+---
+...
+box.schema.user.grant('user', 'write', 'space', '_index')
+---
+...
+box.schema.user.grant('user', 'write', 'space', '_space_sequence')
 ---
 ...
+box.schema.user.grant('user', 'read', 'space', '_index')
+---
+...
+box.schema.user.grant('user', 'read', 'space', '_space_sequence')
+---
+...
+box.session.su('user')
+---
+...
+_ = s2:create_index('pk', {sequence = 'seq1'}) -- error
+---
+- error: Read access to sequence 'seq1' is denied for user 'user'
+...
 s1.index.pk:alter({sequence = 'seq1'}) -- error
 ---
 - error: Alter access to space 'space1' is denied for user 'user'
 ...
 box.space._space_sequence:replace{s1.id, sq1.id, false} -- error
 ---
-- error: Alter access to space 'space1' is denied for user 'user'
+- error: Read access to sequence 'seq1' is denied for user 'user'
 ...
 box.space._space_sequence:replace{s1.id, sq2.id, false} -- error
 ---
@@ -1558,7 +1622,7 @@ box.space._space_sequence:replace{s1.id, sq2.id, false} -- error
 ...
 box.space._space_sequence:replace{s2.id, sq1.id, false} -- error
 ---
-- error: Alter access to sequence 'seq1' is denied for user 'user'
+- error: Read access to sequence 'seq1' is denied for user 'user'
 ...
 s2.index.pk:alter({sequence = 'seq2'}) -- ok
 ---
@@ -1569,10 +1633,22 @@ box.session.su('admin')
 -- If the user owns a sequence attached to a space,
 -- it can use it for auto increment, otherwise it
 -- needs privileges.
-box.schema.user.revoke('user', 'read,write', 'universe')
+box.schema.user.revoke('user', 'write', 'space', '_index')
+---
+...
+box.schema.user.revoke('user', 'write', 'space', '_space_sequence')
+---
+...
+box.schema.user.revoke('user', 'read', 'space', '_space')
+---
+...
+box.schema.user.revoke('user', 'read', 'space', '_sequence')
+---
+...
+box.schema.user.revoke('user', 'read', 'space', '_index')
 ---
 ...
-box.schema.user.revoke('user', 'create', 'universe')
+box.schema.user.revoke('user', 'read', 'space', '_space_sequence')
 ---
 ...
 box.session.su('user')
@@ -1680,7 +1756,16 @@ s:drop()
 ---
 ...
 -- When a user is dropped, all his sequences are dropped as well.
-box.schema.user.grant('user', 'read,write,create', 'universe')
+box.schema.user.grant('user', 'write', 'space', '_sequence')
+---
+...
+box.schema.user.grant('user', 'read', 'space', '_sequence')
+---
+...
+box.schema.user.grant('user', 'write', 'space', '_space_sequence')
+---
+...
+box.schema.user.grant('user', 'create', 'sequence')
 ---
 ...
 box.session.su('user')
@@ -1710,10 +1795,25 @@ box.schema.user.create('user1')
 box.schema.user.create('user2')
 ---
 ...
-box.schema.user.grant('user1', 'read,write,create', 'universe')
+box.schema.user.grant('user1', 'create', 'sequence')
+---
+...
+box.schema.user.grant('user1', 'write', 'space', '_sequence')
+---
+...
+box.schema.user.grant('user1', 'read', 'space', '_sequence')
+---
+...
+box.schema.user.grant('user1', 'read', 'space', '_user')
+---
+...
+box.schema.user.grant('user1', 'write', 'space', '_sequence_data')
+---
+...
+box.schema.user.grant('user1', 'write', 'space', '_priv')
 ---
 ...
-box.schema.user.grant('user2', 'read,write,create', 'universe')
+box.schema.user.grant('user2', 'read,write', 'universe')
 ---
 ...
 box.session.su('user1')
diff --git a/test/box/sequence.test.lua b/test/box/sequence.test.lua
index c119459b3..00261e397 100644
--- a/test/box/sequence.test.lua
+++ b/test/box/sequence.test.lua
@@ -488,16 +488,21 @@ sq:alter{step = 2} -- error
 sq:drop() -- error
 box.session.su('admin')
 sq:drop()
+box.schema.user.revoke('user', 'read,write', 'universe')
 
 -- A user can alter/use sequences that he owns.
-box.schema.user.grant('user', 'create', 'universe')
+box.schema.user.grant('user', 'create', 'sequence')
+box.schema.user.grant('user', 'write', 'space', '_sequence')
+box.schema.user.grant('user', 'write', 'space', '_sequence_data')
 box.session.su('user')
 sq = box.schema.sequence.create('seq')
 sq:alter{step = 2} -- ok
 sq:drop() -- ok
 sq = box.schema.sequence.create('seq')
 box.session.su('admin')
-box.schema.user.revoke('user', 'read,write,create', 'universe')
+box.schema.user.revoke('user', 'create', 'sequence')
+box.schema.user.revoke('user', 'write', 'space', '_sequence')
+box.schema.user.revoke('user', 'write', 'space', '_sequence_data')
 box.session.su('user')
 sq:set(100) -- ok - user owns the sequence
 sq:next() -- ok
@@ -505,17 +510,34 @@ sq:reset() -- ok
 box.session.su('admin')
 sq:drop()
 
--- A sequence can be attached to a space only if the user owns both.
+-- A sequence can be attached to a space only if the user has
+-- alter privilege on space and read/write on sequence.
 sq1 = box.schema.sequence.create('seq1')
 s1 = box.schema.space.create('space1')
 _ = s1:create_index('pk')
-box.schema.user.grant('user', 'read,write', 'universe')
-box.schema.user.grant('user', 'create', 'universe')
+box.schema.user.grant('user', 'write', 'space', '_sequence')
+box.schema.user.grant('user', 'write', 'space', '_sequence_data')
+box.schema.user.grant('user', 'write', 'space', '_schema')
+box.schema.user.grant('user', 'write', 'space', '_space')
+box.schema.user.grant('user', 'create', 'space')
+box.schema.user.grant('user', 'create', 'sequence')
 box.session.su('user')
 sq2 = box.schema.sequence.create('seq2')
 s2 = box.schema.space.create('space2')
--- fixme: no error on using another user's sequence
-_ = s2:create_index('pk', {sequence = 'seq1'})
+
+box.session.su('admin')
+box.schema.user.revoke('user', 'create', 'space')
+box.schema.user.revoke('user', 'create', 'sequence')
+box.schema.user.revoke('user', 'write', 'space', '_sequence')
+box.schema.user.revoke('user', 'write', 'space', '_sequence_data')
+box.schema.user.revoke('user', 'write', 'space', '_schema')
+box.schema.user.revoke('user', 'write', 'space', '_space')
+box.schema.user.grant('user', 'write', 'space', '_index')
+box.schema.user.grant('user', 'write', 'space', '_space_sequence')
+box.schema.user.grant('user', 'read', 'space', '_index')
+box.schema.user.grant('user', 'read', 'space', '_space_sequence')
+box.session.su('user')
+_ = s2:create_index('pk', {sequence = 'seq1'}) -- error
 s1.index.pk:alter({sequence = 'seq1'}) -- error
 box.space._space_sequence:replace{s1.id, sq1.id, false} -- error
 box.space._space_sequence:replace{s1.id, sq2.id, false} -- error
@@ -526,8 +548,12 @@ box.session.su('admin')
 -- If the user owns a sequence attached to a space,
 -- it can use it for auto increment, otherwise it
 -- needs privileges.
-box.schema.user.revoke('user', 'read,write', 'universe')
-box.schema.user.revoke('user', 'create', 'universe')
+box.schema.user.revoke('user', 'write', 'space', '_index')
+box.schema.user.revoke('user', 'write', 'space', '_space_sequence')
+box.schema.user.revoke('user', 'read', 'space', '_space')
+box.schema.user.revoke('user', 'read', 'space', '_sequence')
+box.schema.user.revoke('user', 'read', 'space', '_index')
+box.schema.user.revoke('user', 'read', 'space', '_space_sequence')
 box.session.su('user')
 s2:insert{nil, 1} -- ok: {1, 1}
 box.session.su('admin')
@@ -563,7 +589,10 @@ box.session.su('admin')
 s:drop()
 
 -- When a user is dropped, all his sequences are dropped as well.
-box.schema.user.grant('user', 'read,write,create', 'universe')
+box.schema.user.grant('user', 'write', 'space', '_sequence')
+box.schema.user.grant('user', 'read', 'space', '_sequence')
+box.schema.user.grant('user', 'write', 'space', '_space_sequence')
+box.schema.user.grant('user', 'create', 'sequence')
 box.session.su('user')
 _ = box.schema.sequence.create('test1')
 _ = box.schema.sequence.create('test2')
@@ -575,8 +604,13 @@ box.sequence
 -- to a sequence.
 box.schema.user.create('user1')
 box.schema.user.create('user2')
-box.schema.user.grant('user1', 'read,write,create', 'universe')
-box.schema.user.grant('user2', 'read,write,create', 'universe')
+box.schema.user.grant('user1', 'create', 'sequence')
+box.schema.user.grant('user1', 'write', 'space', '_sequence')
+box.schema.user.grant('user1', 'read', 'space', '_sequence')
+box.schema.user.grant('user1', 'read', 'space', '_user')
+box.schema.user.grant('user1', 'write', 'space', '_sequence_data')
+box.schema.user.grant('user1', 'write', 'space', '_priv')
+box.schema.user.grant('user2', 'read,write', 'universe')
 box.session.su('user1')
 sq = box.schema.sequence.create('test')
 box.session.su('user2')
-- 
2.15.2 (Apple Git-101.1)

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [tarantool-patches] [PATCH 2/4] Add entities user, role to access control.
  2018-07-17 15:47 [tarantool-patches] [PATCH 0/4] Fixes in access control and privileges Serge Petrenko
  2018-07-17 15:47 ` [tarantool-patches] [PATCH 1/4] Make access_check_ddl check for entity privileges Serge Petrenko
@ 2018-07-17 15:47 ` Serge Petrenko
  2018-07-17 15:47 ` [tarantool-patches] [PATCH 3/4] Add single object privilege checks to access_check_ddl Serge Petrenko
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 11+ messages in thread
From: Serge Petrenko @ 2018-07-17 15:47 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Serge Petrenko

Previously the only existing entities in access control were space,
funciton and sequence. Added user and role entities, so it is now
possible to create users or roles without create privilege on universe.
Also added all the needed checks and modified tests accordingly.

Closes #3524
---
 src/box/alter.cc            | 72 +++++++++++++++++++++++++++++++++------------
 src/box/lua/schema.lua      | 27 ++++++++++-------
 src/box/schema.h            |  6 ++++
 src/box/user.cc             | 25 +++++++++++++++-
 test/box/access.result      | 20 ++++++-------
 test/box/access.test.lua    | 15 ++++------
 test/box/access_misc.result |  2 +-
 test/box/role.result        | 25 ++++++++++++++--
 test/box/role.test.lua      | 12 ++++++--
 9 files changed, 150 insertions(+), 54 deletions(-)

diff --git a/src/box/alter.cc b/src/box/alter.cc
index bd50e457a..6293dcc50 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -2186,7 +2186,7 @@ on_replace_dd_user(struct trigger * /* trigger */, void *event)
 	struct user *old_user = user_by_id(uid);
 	if (new_tuple != NULL && old_user == NULL) { /* INSERT */
 		struct user_def *user = user_def_new_from_tuple(new_tuple);
-		access_check_ddl(user->name, user->owner, SC_USER, PRIV_C, true);
+		access_check_ddl(user->name, user->owner, user->type, PRIV_C, true);
 		auto def_guard = make_scoped_guard([=] { free(user); });
 		(void) user_cache_replace(user);
 		def_guard.is_active = false;
@@ -2195,7 +2195,7 @@ on_replace_dd_user(struct trigger * /* trigger */, void *event)
 		txn_on_rollback(txn, on_rollback);
 	} else if (new_tuple == NULL) { /* DELETE */
 		access_check_ddl(old_user->def->name, old_user->def->owner,
-				 SC_USER, PRIV_D, true);
+				 old_user->def->type, PRIV_D, true);
 		/* Can't drop guest or super user */
 		if (uid <= (uint32_t) BOX_SYSTEM_USER_ID_MAX || uid == SUPER) {
 			tnt_raise(ClientError, ER_DROP_USER,
@@ -2221,6 +2221,14 @@ on_replace_dd_user(struct trigger * /* trigger */, void *event)
 		 * correct.
 		 */
 		struct user_def *user = user_def_new_from_tuple(new_tuple);
+		/* Do not allow changes for system users and roles. */
+		struct credentials *cr = effective_user();
+		if ((uid <= (uint32_t) BOX_SYSTEM_USER_ID_MAX || uid == SUPER) &&
+		    cr->uid != ADMIN) {
+			struct user *current_user = user_find_xc(cr->uid);
+			tnt_raise(AccessDeniedError, "alter", "user or role",
+				  old_user->def->name, current_user->def->name);
+		}
 		access_check_ddl(user->name, user->uid, SC_USER, PRIV_A,
 				 true);
 		auto def_guard = make_scoped_guard([=] { free(user); });
@@ -2641,26 +2649,49 @@ priv_def_check(struct priv_def *priv, enum priv_type priv_type)
 	}
 	case SC_ROLE:
 	{
-		struct user *role = user_by_id(priv->object_id);
-		if (role == NULL || role->def->type != SC_ROLE) {
-			tnt_raise(ClientError, ER_NO_SUCH_ROLE,
-				  role ? role->def->name :
-				  int2str(priv->object_id));
-		}
-		/*
-		 * Only the creator of the role can grant or revoke it.
-		 * Everyone can grant 'PUBLIC' role.
-		 */
-		if (role->def->owner != grantor->def->uid &&
-		    grantor->def->uid != ADMIN &&
-		    (role->def->uid != PUBLIC || priv->access != PRIV_X)) {
+		if (priv->object_id != 0) {
+			struct user *role = user_by_id(priv->object_id);
+			if (role == NULL || role->def->type != SC_ROLE) {
+				tnt_raise(ClientError, ER_NO_SUCH_ROLE,
+					  role ? role->def->name :
+					  int2str(priv->object_id));
+			}
+			/*
+			 * Only the creator of the role can grant or revoke it.
+			 * Everyone can grant 'PUBLIC' role.
+			 */
+			if (role->def->owner != grantor->def->uid &&
+			    grantor->def->uid != ADMIN &&
+			    (role->def->uid != PUBLIC || priv->access != PRIV_X)) {
+				tnt_raise(AccessDeniedError,
+					  priv_name(priv_type),
+					  schema_object_name(SC_ROLE), name,
+					  grantor->def->name);
+			}
+			/* Not necessary to do during revoke, but who cares. */
+			role_check(grantee, role);
+		} else if (grantor->def->uid != ADMIN) {
+			/* only admin may grant privileges on an entire entity. */
 			tnt_raise(AccessDeniedError,
 				  priv_name(priv_type),
 				  schema_object_name(SC_ROLE), name,
 				  grantor->def->name);
 		}
-		/* Not necessary to do during revoke, but who cares. */
-		role_check(grantee, role);
+		break;
+	}
+	case SC_USER:
+	{
+		struct user *user = NULL;
+		if (priv->object_id != 0)
+			user = user_by_id(priv->object_id);
+		if ((user == NULL || user->def->owner != grantor->def->uid) &&
+		    grantor->def->uid != ADMIN) {
+			tnt_raise(AccessDeniedError,
+				  priv_name(priv_type),
+				  schema_object_name(SC_USER), name,
+				  grantor->def->name);
+		}
+		break;
 	}
 	default:
 		break;
@@ -2681,7 +2712,12 @@ grant_or_revoke(struct priv_def *priv)
 	struct user *grantee = user_by_id(priv->grantee_id);
 	if (grantee == NULL)
 		return;
-	if (priv->object_type == SC_ROLE) {
+	/*
+	 * Grant a role to a user only when privilege type is 'execute'
+	 * and the role is specified.
+	 */
+	if (priv->object_type == SC_ROLE && !(priv->access & ~PRIV_X) &&
+	    priv->object_id != 0) {
 		struct user *role = user_by_id(priv->object_id);
 		if (role == NULL || role->def->type != SC_ROLE)
 			return;
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index ef544c879..4b7a14411 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -1739,6 +1739,8 @@ local priv_object_combo = {
                            box.priv.C, box.priv.D),
     ["role"]     = bit.bor(box.priv.X, box.priv.U,
                            box.priv.C, box.priv.D),
+    ["user"]	 = bit.bor(box.priv.C, box.priv.U,
+                           box.priv.D),
 }
 
 --
@@ -1842,21 +1844,25 @@ local function object_resolve(object_type, object_name)
         end
         return seq
     end
-    if object_type == 'role' then
+    if object_type == 'role' or object_type == 'user' then
         local _vuser = box.space[box.schema.VUSER_ID]
-        local role
+        local role_or_user
+        if object_name == nil or object_name == 0 then
+            return 0
+        end
         if type(object_name) == 'string' then
-            role = _vuser.index.name:get{object_name}
+            role_or_user = _vuser.index.name:get{object_name}
         else
-            role = _vuser:get{object_name}
+            role_or_user = _vuser:get{object_name}
         end
-        if role and role[4] == 'role' then
-            return role[1]
-        else
+        if role_or_user and role_or_user[4] == object_type then
+            return role_or_user[1]
+        elseif object_type == 'role' then
             box.error(box.error.NO_SUCH_ROLE, object_name)
-        end
+        else
+            box.error(box.error.NO_SUCH_USER, object_name)
+	end
     end
-
     box.error(box.error.UNKNOWN_SCHEMA_OBJECT, object_type)
 end
 
@@ -2102,7 +2108,8 @@ local function grant(uid, name, privilege, object_type,
     if privilege_hex ~= old_privilege then
         _priv:replace{options.grantor, uid, object_type, oid, privilege_hex}
     elseif not options.if_not_exists then
-            if object_type == 'role' then
+            if object_type == 'role' and object_name ~= nil and
+	       object_name ~= 0 then
                 box.error(box.error.ROLE_GRANTED, name, object_name)
             else
                 box.error(box.error.PRIV_GRANTED, name, privilege,
diff --git a/src/box/schema.h b/src/box/schema.h
index 0822262d0..f78ea43cc 100644
--- a/src/box/schema.h
+++ b/src/box/schema.h
@@ -240,6 +240,8 @@ struct on_access_denied_ctx {
 struct entity_access {
        struct access space[BOX_USER_MAX];
        struct access function[BOX_USER_MAX];
+       struct access user[BOX_USER_MAX];
+       struct access role[BOX_USER_MAX];
        struct access sequence[BOX_USER_MAX];
 };
 
@@ -255,6 +257,10 @@ entity_access_get(enum schema_object_type type)
                return entity_access.space;
        case SC_FUNCTION:
                return entity_access.function;
+       case SC_USER:
+	       return entity_access.user;
+       case SC_ROLE:
+	       return entity_access.role;
        case SC_SEQUENCE:
                return entity_access.sequence;
        default:
diff --git a/src/box/user.cc b/src/box/user.cc
index fbf06566a..4edef1d5f 100644
--- a/src/box/user.cc
+++ b/src/box/user.cc
@@ -229,6 +229,29 @@ access_find(struct priv_def *priv)
 			access = func->access;
 		break;
 	}
+	case SC_USER:
+	{
+		/*
+		 * user ID 0 is shared between user 'guest' and granting
+		 * privileges upon whole entity user. This is not a problem,
+		 * since we don't want to grant privileges on any system user,
+		 * including 'guest'.
+		 */
+		if(priv->object_id == 0) {
+			access = entity_access.user;
+			break;
+		}
+		/* No grants on a single object user yet. */
+	}
+	case SC_ROLE:
+	{
+		/* Tha same remark as in case SC_USER applies. */
+		if (priv->object_id == 0) {
+			access = entity_access.role;
+			break;
+		}
+		/* No grants on a single object role yet. */
+	}
 	case SC_SEQUENCE:
 	{
 		if (priv->object_id == 0) {
@@ -315,7 +338,7 @@ user_reload_privs(struct user *user)
 			 * Skip role grants, we're only
 			 * interested in real objects.
 			 */
-			if (priv.object_type != SC_ROLE)
+			if (priv.object_type != SC_ROLE || !(priv.access & PRIV_X))
 				user_grant_priv(user, &priv);
 		}
 	}
diff --git a/test/box/access.result b/test/box/access.result
index f4669a4a3..31095aec2 100644
--- a/test/box/access.result
+++ b/test/box/access.result
@@ -878,8 +878,6 @@ box.schema.user.create('test')
 box.schema.user.grant('test', 'read', 'space', '_collation')
 ---
 ...
---box.schema.user.grant('test', 'write', 'space', '_collation')
--- FIXME: granting create on 'collation' only doesn't work
 box.schema.user.grant('test', 'create', 'universe')
 ---
 ...
@@ -1427,16 +1425,18 @@ box.session.su("admin")
 box.schema.user.grant('tester', 'write', 'universe')
 ---
 ...
--- no entity user currently, so have to grant create
--- on universe in order to create a user.
-box.schema.user.grant('tester', 'create', 'universe')
+box.schema.user.grant('tester', 'create', 'user')
+---
+...
+box.schema.user.grant('tester', 'create', 'space')
+---
+...
+box.schema.user.grant('tester', 'create', 'function')
+---
+...
+box.schema.user.grant('tester', 'create' , 'sequence')
 ---
 ...
--- this should work instead:
---box.schema.user.grant('tester', 'create', 'user')
---box.schema.user.grant('tester', 'create', 'space')
---box.schema.user.grant('tester', 'create', 'function')
---box.schema.user.grant('tester', 'create' , 'sequence')
 box.schema.user.grant('tester', 'read', 'space', '_sequence')
 ---
 ...
diff --git a/test/box/access.test.lua b/test/box/access.test.lua
index 9ae0e1114..9b7510e64 100644
--- a/test/box/access.test.lua
+++ b/test/box/access.test.lua
@@ -341,8 +341,7 @@ c:close()
 session = box.session
 box.schema.user.create('test')
 box.schema.user.grant('test', 'read', 'space', '_collation')
---box.schema.user.grant('test', 'write', 'space', '_collation')
--- FIXME: granting create on 'collation' only doesn't work
+
 box.schema.user.grant('test', 'create', 'universe')
 session.su('test')
 box.internal.collation.create('test', 'ICU', 'ru_RU')
@@ -538,14 +537,10 @@ box.session.su("admin")
 -- tables from ddl
 --
 box.schema.user.grant('tester', 'write', 'universe')
--- no entity user currently, so have to grant create
--- on universe in order to create a user.
-box.schema.user.grant('tester', 'create', 'universe')
--- this should work instead:
---box.schema.user.grant('tester', 'create', 'user')
---box.schema.user.grant('tester', 'create', 'space')
---box.schema.user.grant('tester', 'create', 'function')
---box.schema.user.grant('tester', 'create' , 'sequence')
+box.schema.user.grant('tester', 'create', 'user')
+box.schema.user.grant('tester', 'create', 'space')
+box.schema.user.grant('tester', 'create', 'function')
+box.schema.user.grant('tester', 'create' , 'sequence')
 box.schema.user.grant('tester', 'read', 'space', '_sequence')
 box.session.su("tester")
 -- successful create
diff --git a/test/box/access_misc.result b/test/box/access_misc.result
index 839b576ac..0f4892533 100644
--- a/test/box/access_misc.result
+++ b/test/box/access_misc.result
@@ -361,7 +361,7 @@ testuser_uid = session.uid()
 ...
 _ = box.space._user:delete(2)
 ---
-- error: Drop access to user 'public' is denied for user 'testuser'
+- error: Drop access to role 'public' is denied for user 'testuser'
 ...
 box.space._user:select(1)
 ---
diff --git a/test/box/role.result b/test/box/role.result
index 806cea90b..243c7bc6c 100644
--- a/test/box/role.result
+++ b/test/box/role.result
@@ -214,7 +214,22 @@ box.schema.role.drop('test')
 box.schema.user.grant('grantee', 'liaison')
 ---
 ...
-box.schema.user.grant('test', 'read,write,create', 'universe')
+box.schema.user.grant('test', 'read,write', 'space', '_priv')
+---
+...
+box.schema.user.grant('test', 'write', 'space', '_schema')
+---
+...
+box.schema.user.grant('test', 'create', 'space')
+---
+...
+box.schema.user.grant('test', 'read,write', 'space', '_space')
+---
+...
+box.schema.user.grant('test', 'write', 'space', '_index')
+---
+...
+box.schema.user.grant('test', 'read', 'space', '_user')
 ---
 ...
 box.session.su('test')
@@ -635,7 +650,13 @@ box.schema.user.create('user')
 box.schema.user.create('grantee')
 ---
 ...
-box.schema.user.grant('user', 'read,write,execute,create', 'universe')
+box.schema.user.grant('user', 'read,write', 'space', '_user')
+---
+...
+box.schema.user.grant('user', 'read,write', 'space', '_priv')
+---
+...
+box.schema.user.grant('user', 'create', 'role')
 ---
 ...
 box.session.su('user')
diff --git a/test/box/role.test.lua b/test/box/role.test.lua
index e97339f49..9845f4c4c 100644
--- a/test/box/role.test.lua
+++ b/test/box/role.test.lua
@@ -69,7 +69,13 @@ box.schema.role.revoke('test', 'liaison')
 box.schema.role.drop('test')
 
 box.schema.user.grant('grantee', 'liaison')
-box.schema.user.grant('test', 'read,write,create', 'universe')
+
+box.schema.user.grant('test', 'read,write', 'space', '_priv')
+box.schema.user.grant('test', 'write', 'space', '_schema')
+box.schema.user.grant('test', 'create', 'space')
+box.schema.user.grant('test', 'read,write', 'space', '_space')
+box.schema.user.grant('test', 'write', 'space', '_index')
+box.schema.user.grant('test', 'read', 'space', '_user')
 box.session.su('test')
 s = box.schema.space.create('test')
 _ = s:create_index('i1')
@@ -248,7 +254,9 @@ box.schema.role.drop("role10")
 box.schema.user.create('user')
 box.schema.user.create('grantee')
 
-box.schema.user.grant('user', 'read,write,execute,create', 'universe')
+box.schema.user.grant('user', 'read,write', 'space', '_user')
+box.schema.user.grant('user', 'read,write', 'space', '_priv')
+box.schema.user.grant('user', 'create', 'role')
 box.session.su('user')
 box.schema.role.create('role')
 box.session.su('admin')
-- 
2.15.2 (Apple Git-101.1)

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [tarantool-patches] [PATCH 3/4] Add single object privilege checks to access_check_ddl.
  2018-07-17 15:47 [tarantool-patches] [PATCH 0/4] Fixes in access control and privileges Serge Petrenko
  2018-07-17 15:47 ` [tarantool-patches] [PATCH 1/4] Make access_check_ddl check for entity privileges Serge Petrenko
  2018-07-17 15:47 ` [tarantool-patches] [PATCH 2/4] Add entities user, role to access control Serge Petrenko
@ 2018-07-17 15:47 ` Serge Petrenko
  2018-07-17 15:47 ` [tarantool-patches] [PATCH 4/4] Add a privilege upgrade script and update tests Serge Petrenko
       [not found] ` <cover.1531843622.git.sergepetrenko@tarantool.org>
  4 siblings, 0 replies; 11+ messages in thread
From: Serge Petrenko @ 2018-07-17 15:47 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Serge Petrenko

access_check_ddl() didn't check for single object privileges, e.g. user
with alter access on a space couldn't create an index in this space. It
would only succeed if it had alter on entire entity space.
Fix this by adding single object privilege checks to access_check_ddl and
adding access cache to struct user, to hold other users' privileges on it.

Also checking for single object privilege made it possible to grant
every user alter privilege on itself, so that a user may change its own
password (previously it was possible because of a hack). Removed the
hack, and added grant alter to itself upon user creation.
Modified tests accordingly, and added a couple of test cases.

Closes #3530
---
 src/box/alter.cc         | 123 ++++++++++++++++++++++----------
 src/box/lua/schema.lua   |   5 +-
 src/box/user.cc          |  10 ++-
 src/box/user.h           |   2 +
 test/box/access.result   | 182 +++++++++++++++++++++++++++++++++++++++++++++++
 test/box/access.test.lua |  53 ++++++++++++++
 test/box/role.result     |   9 +++
 test/box/sequence.result |   3 +
 8 files changed, 347 insertions(+), 40 deletions(-)

diff --git a/src/box/alter.cc b/src/box/alter.cc
index 6293dcc50..54a09664b 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -62,7 +62,8 @@
 /* {{{ Auxiliary functions and methods. */
 
 static void
-access_check_ddl(const char *name, uint32_t owner_uid,
+access_check_ddl(const char *name, uint32_t object_id,
+		 uint32_t owner_uid,
 		 enum schema_object_type type,
 		 enum priv_type priv_type,
 		 bool is_17_compat_mode)
@@ -103,7 +104,48 @@ access_check_ddl(const char *name, uint32_t owner_uid,
 	 */
 	if (access == 0 || (is_owner && !(access & (PRIV_U | PRIV_C))))
 		return; /* Access granted. */
-
+	/*
+	 * You can't grant CREATE privilege to a non-existing object.
+	 * USAGE can be granted only globally.
+	 */
+	if (!(access & (PRIV_U | PRIV_C))) {
+		/* Check for privileges on a single object. */
+		switch (type) {
+		case SC_SPACE:
+		{
+			struct space *space = space_by_id(object_id);
+			if (space)
+				access &= ~space->access[cr->auth_token].effective;
+			break;
+		}
+		case SC_FUNCTION:
+		{
+			struct func *func = func_by_id(object_id);
+			if (func)
+				access &= ~func->access[cr->auth_token].effective;
+			break;
+		}
+		case SC_USER:
+		case SC_ROLE:
+		{
+			struct user *user_or_role = user_by_id(object_id);
+			if (user_or_role)
+				access &= ~user_or_role->access[cr->auth_token].effective;
+			break;
+		}
+		case SC_SEQUENCE:
+		{
+			struct sequence *seq = sequence_by_id(object_id);
+			if (seq)
+				access &= ~seq->access[cr->auth_token].effective;
+			break;
+		}
+		default:
+			break;
+		}
+	}
+	if (access == 0)
+	    return; /* Access granted. */
 	/* Create a meaningful error message. */
 	struct user *user = user_find_xc(cr->uid);
 	const char *object_name;
@@ -1590,7 +1632,8 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 		struct space_def *def =
 			space_def_new_from_tuple(new_tuple, ER_CREATE_SPACE,
 						 region);
-		access_check_ddl(def->name, def->uid, SC_SPACE, PRIV_C, true);
+		access_check_ddl(def->name, def->id, def->uid, SC_SPACE,
+				 PRIV_C, true);
 		auto def_guard =
 			make_scoped_guard([=] { space_def_delete(def); });
 		RLIST_HEAD(empty_list);
@@ -1623,8 +1666,8 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 			txn_alter_trigger_new(on_create_space_rollback, space);
 		txn_on_rollback(txn, on_rollback);
 	} else if (new_tuple == NULL) { /* DELETE */
-		access_check_ddl(old_space->def->name, old_space->def->uid,
-				 SC_SPACE, PRIV_D, true);
+		access_check_ddl(old_space->def->name, old_space->def->id,
+				 old_space->def->uid, SC_SPACE, PRIV_D, true);
 		/* Verify that the space is empty (has no indexes) */
 		if (old_space->index_count) {
 			tnt_raise(ClientError, ER_DROP_SPACE,
@@ -1669,7 +1712,8 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 		struct space_def *def =
 			space_def_new_from_tuple(new_tuple, ER_ALTER_SPACE,
 						 region);
-		access_check_ddl(def->name, def->uid, SC_SPACE, PRIV_A, true);
+		access_check_ddl(def->name, def->id, def->uid, SC_SPACE,
+				 PRIV_A, true);
 		auto def_guard =
 			make_scoped_guard([=] { space_def_delete(def); });
 		if (def->id != space_id(old_space))
@@ -1773,8 +1817,8 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
 	struct space *old_space = space_cache_find_xc(id);
 	bool have_alter = true;
 	try {
-		access_check_ddl(old_space->def->name, old_space->def->uid, SC_SPACE,
-				 PRIV_A, true);
+		access_check_ddl(old_space->def->name, old_space->def->id,
+				 old_space->def->uid, SC_SPACE, PRIV_A, true);
 	} catch(AccessDeniedError *e) {
 		/*
 		 * We need alter access on space in case of creating or
@@ -1847,7 +1891,8 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
 		 * dropping an index, check for drop on space.
 		 */
 		if (!have_alter)
-		    access_check_ddl(old_space->def->name, old_space->def->uid,
+		    access_check_ddl(old_space->def->name, old_space->def->id,
+				     old_space->def->uid,
 				     SC_SPACE, PRIV_D, true);
 		alter_space_move_indexes(alter, 0, iid);
 		(void) new DropIndex(alter, old_index->def);
@@ -2186,7 +2231,8 @@ on_replace_dd_user(struct trigger * /* trigger */, void *event)
 	struct user *old_user = user_by_id(uid);
 	if (new_tuple != NULL && old_user == NULL) { /* INSERT */
 		struct user_def *user = user_def_new_from_tuple(new_tuple);
-		access_check_ddl(user->name, user->owner, user->type, PRIV_C, true);
+		access_check_ddl(user->name, user->uid, user->owner, user->type,
+				 PRIV_C, true);
 		auto def_guard = make_scoped_guard([=] { free(user); });
 		(void) user_cache_replace(user);
 		def_guard.is_active = false;
@@ -2194,7 +2240,8 @@ on_replace_dd_user(struct trigger * /* trigger */, void *event)
 			txn_alter_trigger_new(user_cache_remove_user, NULL);
 		txn_on_rollback(txn, on_rollback);
 	} else if (new_tuple == NULL) { /* DELETE */
-		access_check_ddl(old_user->def->name, old_user->def->owner,
+		access_check_ddl(old_user->def->name, old_user->def->uid,
+				 old_user->def->owner,
 				 old_user->def->type, PRIV_D, true);
 		/* Can't drop guest or super user */
 		if (uid <= (uint32_t) BOX_SYSTEM_USER_ID_MAX || uid == SUPER) {
@@ -2229,8 +2276,8 @@ on_replace_dd_user(struct trigger * /* trigger */, void *event)
 			tnt_raise(AccessDeniedError, "alter", "user or role",
 				  old_user->def->name, current_user->def->name);
 		}
-		access_check_ddl(user->name, user->uid, SC_USER, PRIV_A,
-				 true);
+		access_check_ddl(user->name, user->uid, user->owner, SC_USER,
+				 PRIV_A, true);
 		auto def_guard = make_scoped_guard([=] { free(user); });
 		struct trigger *on_commit =
 			txn_alter_trigger_new(user_cache_alter_user, NULL);
@@ -2332,7 +2379,8 @@ on_replace_dd_func(struct trigger * /* trigger */, void *event)
 	struct func *old_func = func_by_id(fid);
 	if (new_tuple != NULL && old_func == NULL) { /* INSERT */
 		struct func_def *def = func_def_new_from_tuple(new_tuple);
-		access_check_ddl(def->name, def->uid, SC_FUNCTION, PRIV_C, true);
+		access_check_ddl(def->name, def->fid, def->uid, SC_FUNCTION,
+				 PRIV_C, true);
 		auto def_guard = make_scoped_guard([=] { free(def); });
 		func_cache_replace(def);
 		def_guard.is_active = false;
@@ -2346,7 +2394,7 @@ on_replace_dd_func(struct trigger * /* trigger */, void *event)
 		 * Can only delete func if you're the one
 		 * who created it or a superuser.
 		 */
-		access_check_ddl(old_func->def->name, uid, SC_FUNCTION,
+		access_check_ddl(old_func->def->name, fid, uid, SC_FUNCTION,
 				 PRIV_D, true);
 		/* Can only delete func if it has no grants. */
 		if (schema_find_grants("function", old_func->def->fid)) {
@@ -2360,8 +2408,8 @@ on_replace_dd_func(struct trigger * /* trigger */, void *event)
 	} else {                                /* UPDATE, REPLACE */
 		struct func_def *def = func_def_new_from_tuple(new_tuple);
 		auto def_guard = make_scoped_guard([=] { free(def); });
-		access_check_ddl(def->name, def->uid, SC_FUNCTION, PRIV_A,
-				 true);
+		access_check_ddl(def->name, def->fid, def->uid,
+				 SC_FUNCTION, PRIV_A, true);
 		struct trigger *on_commit =
 			txn_alter_trigger_new(func_cache_replace_func, NULL);
 		txn_on_commit(txn, on_commit);
@@ -2517,8 +2565,9 @@ on_replace_dd_collation(struct trigger * /* trigger */, void *event)
 						    BOX_COLLATION_FIELD_ID);
 		struct coll_id *old_coll_id = coll_by_id(old_id);
 		assert(old_coll_id != NULL);
-		access_check_ddl(old_coll_id->name, old_coll_id->owner_id,
-				 SC_COLLATION, PRIV_D, false);
+		access_check_ddl(old_coll_id->name, old_id,
+				 old_coll_id->owner_id, SC_COLLATION, PRIV_D,
+				 false);
 		/*
 		 * Set on_commit/on_rollback triggers after
 		 * deletion from the cache to make trigger logic
@@ -2533,8 +2582,8 @@ on_replace_dd_collation(struct trigger * /* trigger */, void *event)
 		/* INSERT */
 		struct coll_id_def new_def;
 		coll_id_def_new_from_tuple(new_tuple, &new_def);
-		access_check_ddl(new_def.name, new_def.owner_id, SC_COLLATION,
-				 PRIV_C, false);
+		access_check_ddl(new_def.name, new_def.id, new_def.owner_id,
+				 SC_COLLATION, PRIV_C, false);
 		struct coll_id *new_coll_id = coll_id_new(&new_def);
 		if (new_coll_id == NULL)
 			diag_raise();
@@ -2593,8 +2642,8 @@ priv_def_check(struct priv_def *priv, enum priv_type priv_type)
 			  int2str(priv->grantee_id));
 	}
 	const char *name = schema_find_name(priv->object_type, priv->object_id);
-	access_check_ddl(name, grantor->def->uid, priv->object_type, priv_type,
-			 false);
+	access_check_ddl(name, priv->object_id, grantor->def->uid,
+			 priv->object_type, priv_type, false);
 	switch (priv->object_type) {
 	case SC_UNIVERSE:
 		if (grantor->def->uid != ADMIN) {
@@ -3090,8 +3139,8 @@ on_replace_dd_sequence(struct trigger * /* trigger */, void *event)
 		new_def = sequence_def_new_from_tuple(new_tuple,
 						      ER_CREATE_SEQUENCE);
 		assert(sequence_by_id(new_def->id) == NULL);
-		access_check_ddl(new_def->name, new_def->uid, SC_SEQUENCE,
-				 PRIV_C, false);
+		access_check_ddl(new_def->name, new_def->id, new_def->uid,
+				 SC_SEQUENCE, PRIV_C, false);
 		sequence_cache_replace(new_def);
 		alter->new_def = new_def;
 	} else if (old_tuple != NULL && new_tuple == NULL) {	/* DELETE */
@@ -3099,8 +3148,8 @@ on_replace_dd_sequence(struct trigger * /* trigger */, void *event)
 						 BOX_SEQUENCE_DATA_FIELD_ID);
 		struct sequence *seq = sequence_by_id(id);
 		assert(seq != NULL);
-		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
-				 PRIV_D, false);
+		access_check_ddl(seq->def->name, seq->def->id, seq->def->uid,
+				 SC_SEQUENCE, PRIV_D, false);
 		if (space_has_data(BOX_SEQUENCE_DATA_ID, 0, id))
 			tnt_raise(ClientError, ER_DROP_SEQUENCE,
 				  seq->def->name, "the sequence has data");
@@ -3116,8 +3165,8 @@ on_replace_dd_sequence(struct trigger * /* trigger */, void *event)
 						      ER_ALTER_SEQUENCE);
 		struct sequence *seq = sequence_by_id(new_def->id);
 		assert(seq != NULL);
-		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
-				 PRIV_A, false);
+		access_check_ddl(seq->def->name, seq->def->id, seq->def->uid,
+				 SC_SEQUENCE, PRIV_A, false);
 		alter->old_def = seq->def;
 		alter->new_def = new_def;
 	}
@@ -3196,21 +3245,21 @@ on_replace_dd_space_sequence(struct trigger * /* trigger */, void *event)
 		priv_type = PRIV_A;
 	/* Check we have the correct access type on the sequence.  * */
 	if (is_generated || !stmt->new_tuple) {
-		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
-				 priv_type, false);
+		access_check_ddl(seq->def->name, seq->def->id, seq->def->uid,
+				 SC_SEQUENCE, priv_type, false);
 	} else {
 		/*
 		 * In case user wants to attach an existing sequence,
 		 * check that it has read and write access.
 		 */
-		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
-				 PRIV_R, false);
-		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
-				 PRIV_W, false);
+		access_check_ddl(seq->def->name, seq->def->id, seq->def->uid,
+				 SC_SEQUENCE, PRIV_R, false);
+		access_check_ddl(seq->def->name, seq->def->id, seq->def->uid,
+				 SC_SEQUENCE, PRIV_W, false);
 	}
 	/** Check we have alter access on space. */
-	access_check_ddl(space->def->name, space->def->uid, SC_SPACE, PRIV_A,
-			 false);
+	access_check_ddl(space->def->name, space->def->id, space->def->uid,
+			 SC_SPACE, PRIV_A, false);
 
 	struct trigger *on_commit =
 		txn_alter_trigger_new(on_commit_dd_space_sequence, space);
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index 4b7a14411..a098e44fe 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -1740,7 +1740,7 @@ local priv_object_combo = {
     ["role"]     = bit.bor(box.priv.X, box.priv.U,
                            box.priv.C, box.priv.D),
     ["user"]	 = bit.bor(box.priv.C, box.priv.U,
-                           box.priv.D),
+                           box.priv.A, box.priv.D),
 }
 
 --
@@ -2056,6 +2056,9 @@ box.schema.user.create = function(name, opts)
     uid = _user:auto_increment{session.euid(), name, 'user', auth_mech_list}[1]
     -- grant role 'public' to the user
     box.schema.user.grant(uid, 'public')
+    -- grant user 'alter' on itself, so it can
+    -- change its password or username.
+    box.schema.user.grant(uid, 'alter', 'user', uid)
     -- we have to grant global privileges from setuid function, since
     -- only admin has the ownership over universe and we don't have
     -- grant option
diff --git a/src/box/user.cc b/src/box/user.cc
index 4edef1d5f..36e29f478 100644
--- a/src/box/user.cc
+++ b/src/box/user.cc
@@ -241,7 +241,10 @@ access_find(struct priv_def *priv)
 			access = entity_access.user;
 			break;
 		}
-		/* No grants on a single object user yet. */
+		struct user *user = user_by_id(priv->object_id);
+		if (user)
+			access = user->access;
+		break;
 	}
 	case SC_ROLE:
 	{
@@ -250,7 +253,10 @@ access_find(struct priv_def *priv)
 			access = entity_access.role;
 			break;
 		}
-		/* No grants on a single object role yet. */
+		struct user *role = user_by_id(priv->object_id);
+		if (role)
+			access = role->access;
+		break;
 	}
 	case SC_SEQUENCE:
 	{
diff --git a/src/box/user.h b/src/box/user.h
index 07c4dc504..069d9b77e 100644
--- a/src/box/user.h
+++ b/src/box/user.h
@@ -88,6 +88,8 @@ struct user
 	bool is_dirty;
 	/** Memory pool for privs */
 	struct region pool;
+	/** Cached runtime access imformation. */
+	struct access access[BOX_USER_MAX];
 };
 
 /** Find user by id. */
diff --git a/test/box/access.result b/test/box/access.result
index 31095aec2..9ea5568a5 100644
--- a/test/box/access.result
+++ b/test/box/access.result
@@ -136,6 +136,9 @@ box.schema.user.revoke('rich', 'read,write', 'universe')
 box.schema.user.revoke('rich', 'public')
 ---
 ...
+box.schema.user.revoke('rich', 'alter', 'user', 'rich')
+---
+...
 box.schema.user.disable("rich")
 ---
 ...
@@ -501,6 +504,7 @@ box.space._priv:select{id}
 ---
 - - [1, 32, 'role', 2, 4]
   - [1, 32, 'universe', 0, 27]
+  - [1, 32, 'user', 32, 128]
 ...
 box.schema.user.grant('user', 'read', 'universe')
 ---
@@ -510,6 +514,7 @@ box.space._priv:select{id}
 ---
 - - [1, 32, 'role', 2, 4]
   - [1, 32, 'universe', 0, 27]
+  - [1, 32, 'user', 32, 128]
 ...
 box.schema.user.revoke('user', 'write', 'universe')
 ---
@@ -518,6 +523,7 @@ box.space._priv:select{id}
 ---
 - - [1, 32, 'role', 2, 4]
   - [1, 32, 'universe', 0, 25]
+  - [1, 32, 'user', 32, 128]
 ...
 box.schema.user.revoke('user', 'read', 'universe')
 ---
@@ -526,6 +532,7 @@ box.space._priv:select{id}
 ---
 - - [1, 32, 'role', 2, 4]
   - [1, 32, 'universe', 0, 24]
+  - [1, 32, 'user', 32, 128]
 ...
 box.schema.user.grant('user', 'write', 'universe')
 ---
@@ -534,6 +541,7 @@ box.space._priv:select{id}
 ---
 - - [1, 32, 'role', 2, 4]
   - [1, 32, 'universe', 0, 26]
+  - [1, 32, 'user', 32, 128]
 ...
 box.schema.user.grant('user', 'read', 'universe')
 ---
@@ -542,6 +550,7 @@ box.space._priv:select{id}
 ---
 - - [1, 32, 'role', 2, 4]
   - [1, 32, 'universe', 0, 27]
+  - [1, 32, 'user', 32, 128]
 ...
 box.schema.user.drop('user')
 ---
@@ -965,6 +974,9 @@ box.schema.user.info('test_user')
   - - session,usage
     - universe
     - 
+  - - alter
+    - user
+    - test_user
 ...
 box.schema.role.info('test_role')
 ---
@@ -995,6 +1007,9 @@ box.schema.user.info('test_user')
   - - session,usage
     - universe
     - 
+  - - alter
+    - user
+    - test_user
 ...
 box.schema.role.info('test_role')
 ---
@@ -1860,3 +1875,170 @@ box.session.su('admin')
 box.schema.user.drop('tester')
 ---
 ...
+--
+-- test case for 3530: do not ignore single object privileges in
+-- access_check_ddl.
+--
+box.schema.user.create("test")
+---
+...
+_ = box.schema.space.create("space1")
+---
+...
+box.schema.user.grant("test", "read", "space", "space1")
+---
+...
+box.schema.user.grant("test", "write", "space", "_index")
+---
+...
+box.session.su("test")
+---
+...
+box.space.space1:create_index("pk")
+---
+- error: Alter access to space 'space1' is denied for user 'test'
+...
+box.session.su("admin")
+---
+...
+box.space.space1.index[0] == nil
+---
+- true
+...
+box.schema.user.grant("test", "alter", "space", "space1")
+---
+...
+box.session.su("test")
+---
+...
+_ = box.space.space1:create_index("pk")
+---
+...
+box.space.space1:insert{5}
+---
+- error: Write access to space 'space1' is denied for user 'test'
+...
+box.session.su("admin")
+---
+...
+box.space.space1.index[0] ~= nil
+---
+- true
+...
+box.space.space1:select{}
+---
+- []
+...
+box.schema.user.grant("test", "write", "space", "space1")
+---
+...
+box.session.su("test")
+---
+...
+box.space.space1:insert{5}
+---
+- [5]
+...
+box.session.su("admin")
+---
+...
+box.space.space1:select{}
+---
+- - [5]
+...
+box.schema.user.drop("test")
+---
+...
+box.space.space1:drop()
+---
+...
+--
+-- test that it is possible to grant privileges on a single user.
+box.schema.user.create("user1")
+---
+...
+box.schema.user.create("user2")
+---
+...
+box.schema.user.create("user3")
+---
+...
+box.schema.user.grant("user1", "write", "space", "_user")
+---
+...
+box.schema.user.grant("user1", "read", "space", "_user")
+---
+...
+box.space._user:select{}
+---
+- - [0, 1, 'guest', 'user', {'chap-sha1': 'vhvewKp0tNyweZQ+cFKAlsyphfg='}]
+  - [1, 1, 'admin', 'user', {}]
+  - [2, 1, 'public', 'role', {}]
+  - [3, 1, 'replication', 'role', {}]
+  - [31, 1, 'super', 'role', {}]
+  - [32, 1, 'user1', 'user', {}]
+  - [33, 1, 'user2', 'user', {}]
+  - [34, 1, 'user3', 'user', {}]
+...
+box.session.su("user1")
+---
+...
+-- can alter itself, but can't alter others without privileges.
+box.schema.user.passwd("user1", "abcd")
+---
+...
+box.schema.user.passwd("user2", "abcd")
+---
+- error: Alter access to user 'user2' is denied for user 'user1'
+...
+box.session.su("admin")
+---
+...
+box.space._user:select{}
+---
+- - [0, 1, 'guest', 'user', {'chap-sha1': 'vhvewKp0tNyweZQ+cFKAlsyphfg='}]
+  - [1, 1, 'admin', 'user', {}]
+  - [2, 1, 'public', 'role', {}]
+  - [3, 1, 'replication', 'role', {}]
+  - [31, 1, 'super', 'role', {}]
+  - [32, 1, 'user1', 'user', {'chap-sha1': 'oVTFJWXp5/lL/Aih/nAmJO2O/9o='}]
+  - [33, 1, 'user2', 'user', {}]
+  - [34, 1, 'user3', 'user', {}]
+...
+box.schema.user.grant("user1", "alter", "user", "user2")
+---
+...
+box.session.su("user1")
+---
+...
+box.schema.user.passwd("user2", "abcd")
+---
+...
+-- still fails
+box.schema.user.passwd("user3", "qewr")
+---
+- error: Alter access to user 'user3' is denied for user 'user1'
+...
+box.session.su("admin")
+---
+...
+box.space._user:select{}
+---
+- - [0, 1, 'guest', 'user', {'chap-sha1': 'vhvewKp0tNyweZQ+cFKAlsyphfg='}]
+  - [1, 1, 'admin', 'user', {}]
+  - [2, 1, 'public', 'role', {}]
+  - [3, 1, 'replication', 'role', {}]
+  - [31, 1, 'super', 'role', {}]
+  - [32, 1, 'user1', 'user', {'chap-sha1': 'oVTFJWXp5/lL/Aih/nAmJO2O/9o='}]
+  - [33, 1, 'user2', 'user', {'chap-sha1': 'oVTFJWXp5/lL/Aih/nAmJO2O/9o='}]
+  - [34, 1, 'user3', 'user', {}]
+...
+box.schema.user.drop("user1")
+---
+...
+box.schema.user.drop("user2")
+---
+...
+box.schema.user.drop("user3")
+---
+...
diff --git a/test/box/access.test.lua b/test/box/access.test.lua
index 9b7510e64..6ad7ee462 100644
--- a/test/box/access.test.lua
+++ b/test/box/access.test.lua
@@ -60,6 +60,7 @@ box.schema.func.drop('dummy')
 box.space['_user']:delete{uid}
 box.schema.user.revoke('rich', 'read,write', 'universe')
 box.schema.user.revoke('rich', 'public')
+box.schema.user.revoke('rich', 'alter', 'user', 'rich')
 box.schema.user.disable("rich")
 -- test double disable is a no op
 box.schema.user.disable("rich")
@@ -726,3 +727,55 @@ _ = box.schema.sequence.create('test_sequence')
 box.session.su('admin')
 box.schema.user.drop('tester')
 
+
+--
+-- test case for 3530: do not ignore single object privileges in
+-- access_check_ddl.
+--
+box.schema.user.create("test")
+_ = box.schema.space.create("space1")
+box.schema.user.grant("test", "read", "space", "space1")
+box.schema.user.grant("test", "write", "space", "_index")
+box.session.su("test")
+box.space.space1:create_index("pk")
+box.session.su("admin")
+box.space.space1.index[0] == nil
+box.schema.user.grant("test", "alter", "space", "space1")
+box.session.su("test")
+_ = box.space.space1:create_index("pk")
+box.space.space1:insert{5}
+box.session.su("admin")
+box.space.space1.index[0] ~= nil
+box.space.space1:select{}
+box.schema.user.grant("test", "write", "space", "space1")
+box.session.su("test")
+box.space.space1:insert{5}
+box.session.su("admin")
+box.space.space1:select{}
+box.schema.user.drop("test")
+box.space.space1:drop()
+
+--
+-- test that it is possible to grant privileges on a single user.
+box.schema.user.create("user1")
+box.schema.user.create("user2")
+box.schema.user.create("user3")
+box.schema.user.grant("user1", "write", "space", "_user")
+box.schema.user.grant("user1", "read", "space", "_user")
+box.space._user:select{}
+box.session.su("user1")
+-- can alter itself, but can't alter others without privileges.
+box.schema.user.passwd("user1", "abcd")
+box.schema.user.passwd("user2", "abcd")
+box.session.su("admin")
+box.space._user:select{}
+box.schema.user.grant("user1", "alter", "user", "user2")
+box.session.su("user1")
+box.schema.user.passwd("user2", "abcd")
+-- still fails
+box.schema.user.passwd("user3", "qewr")
+box.session.su("admin")
+box.space._user:select{}
+box.schema.user.drop("user1")
+box.schema.user.drop("user2")
+box.schema.user.drop("user3")
diff --git a/test/box/role.result b/test/box/role.result
index 243c7bc6c..5666f7ef7 100644
--- a/test/box/role.result
+++ b/test/box/role.result
@@ -49,6 +49,9 @@ box.schema.user.info('tester')
   - - session,usage
     - universe
     - 
+  - - alter
+    - user
+    - tester
 ...
 box.schema.user.grant('tester', 'execute', 'role', 'iddqd')
 ---
@@ -64,6 +67,9 @@ box.schema.user.info('tester')
   - - session,usage
     - universe
     - 
+  - - alter
+    - user
+    - tester
 ...
 -- test granting user to a user
 box.schema.user.grant('tester', 'execute', 'role', 'tester')
@@ -956,6 +962,9 @@ box.schema.user.info('test_user')
   - - session,usage
     - universe
     - 
+  - - alter
+    - user
+    - test_user
 ...
 box.schema.role.info('test_user')
 ---
diff --git a/test/box/sequence.result b/test/box/sequence.result
index 75d5ea1e6..a39a1155f 100644
--- a/test/box/sequence.result
+++ b/test/box/sequence.result
@@ -1362,6 +1362,9 @@ box.schema.user.info()
   - - session,usage
     - universe
     - 
+  - - alter
+    - user
+    - user
 ...
 sq:set(100) -- ok
 ---
-- 
2.15.2 (Apple Git-101.1)

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [tarantool-patches] [PATCH 4/4] Add a privilege upgrade script and update tests.
  2018-07-17 15:47 [tarantool-patches] [PATCH 0/4] Fixes in access control and privileges Serge Petrenko
                   ` (2 preceding siblings ...)
  2018-07-17 15:47 ` [tarantool-patches] [PATCH 3/4] Add single object privilege checks to access_check_ddl Serge Petrenko
@ 2018-07-17 15:47 ` Serge Petrenko
       [not found] ` <cover.1531843622.git.sergepetrenko@tarantool.org>
  4 siblings, 0 replies; 11+ messages in thread
From: Serge Petrenko @ 2018-07-17 15:47 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Serge Petrenko

This patch adds a privilege upgrade script, which runs on upgrade to
1.10 and automatically grants CREATE,ALTER,DROP on objects and entities
to all users, who have READ and WRITE access on them.
Also all tests are rewritten to grant only necessary privileges, not
privileges to universe.

Closes #3539
---
 src/box/lua/upgrade.lua                     |  22 +++
 test/box-tap/auth.test.lua                  |   5 -
 test/box-tap/session.test.lua               |  15 +-
 test/box/access.result                      |   4 +-
 test/box/access.test.lua                    |   4 +-
 test/box/access_bin.result                  |   4 +-
 test/box/access_bin.test.lua                |   4 +-
 test/box/access_escalation.result           |  15 +-
 test/box/access_escalation.test.lua         |  10 +-
 test/box/access_misc.result                 |   4 +-
 test/box/access_misc.test.lua               |   4 +-
 test/box/call.result                        |   4 +-
 test/box/call.test.lua                      |   4 +-
 test/box/errinj.result                      |  30 +++-
 test/box/errinj.test.lua                    |  21 ++-
 test/box/net.box.result                     | 265 ++++++++++++++++++++++++++--
 test/box/net.box.test.lua                   | 121 +++++++++++--
 test/box/net_msg_max.result                 |  13 +-
 test/box/net_msg_max.test.lua               |   9 +-
 test/box/on_replace.result                  |   2 +-
 test/box/on_replace.test.lua                |   2 +-
 test/box/protocol.result                    |   9 +-
 test/box/protocol.test.lua                  |   5 +-
 test/box/push.result                        |  52 +++++-
 test/box/push.test.lua                      |  27 ++-
 test/box/schema_reload.result               |  32 +++-
 test/box/schema_reload.test.lua             |  16 +-
 test/box/sql.result                         |   9 +-
 test/box/sql.test.lua                       |   5 +-
 test/box/stat_net.result                    |   7 +-
 test/box/stat_net.test.lua                  |   5 +-
 test/engine/params.result                   |   6 -
 test/engine/params.test.lua                 |   2 -
 test/engine/replica_join.result             |   6 -
 test/engine/replica_join.test.lua           |   2 -
 test/replication/autobootstrap.result       |  23 ++-
 test/replication/autobootstrap.test.lua     |  10 +-
 test/replication/catch.result               |   6 -
 test/replication/catch.test.lua             |   2 -
 test/replication/errinj.result              |   3 -
 test/replication/errinj.test.lua            |   1 -
 test/replication/gc.result                  |   6 -
 test/replication/gc.test.lua                |   2 -
 test/replication/join_vclock.result         |   6 -
 test/replication/join_vclock.test.lua       |   2 -
 test/replication/skip_conflict_row.result   |   6 -
 test/replication/skip_conflict_row.test.lua |   2 -
 test/vinyl/replica_quota.result             |   6 -
 test/vinyl/replica_quota.test.lua           |   2 -
 test/wal_off/func_max.result                |  25 +--
 test/wal_off/func_max.test.lua              |  19 +-
 test/xlog/errinj.result                     |   9 +-
 test/xlog/errinj.test.lua                   |   4 +-
 test/xlog/misc.result                       |   9 +-
 test/xlog/misc.test.lua                     |   5 +-
 test/xlog/upgrade.result                    |   4 +-
 56 files changed, 672 insertions(+), 225 deletions(-)

diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua
index 0293f6ef8..e738d48e4 100644
--- a/src/box/lua/upgrade.lua
+++ b/src/box/lua/upgrade.lua
@@ -960,8 +960,30 @@ local function create_vsequence_space()
     box.space._vsequence:format(sequence_format)
 end
 
+local function upgrade_users_to_1_10_0()
+    local _priv = box.space[box.schema.PRIV_ID]
+    local _user = box.space[box.schema.USER_ID]
+
+    for _, user in _user:pairs() do
+        if user[0] ~= ADMIN and user[0] ~= SUPER then
+            for _, priv in _priv:pairs(user[0]) do
+                if bit.band(priv[5], box.priv.W) ~= 0 and
+                bit.band(priv[5], box.priv.R) ~= 0 then
+                    local new_privs = bit.bor(box.priv.A, box.priv.D)
+                    if priv[4] == 0 then
+                        new_privs = bit.bor(new_privs, box.priv.C)
+                    end
+                    _priv:update({priv[2], priv[3], priv[4]},
+                                 {{ "|", 5, new_privs}})
+                end
+            end
+        end
+    end
+end
+
 local function upgrade_to_1_10_0()
     create_vsequence_space()
+    upgrade_users_to_1_10_0()
 end
 
 
diff --git a/test/box-tap/auth.test.lua b/test/box-tap/auth.test.lua
index 272bd97dc..4e9879408 100755
--- a/test/box-tap/auth.test.lua
+++ b/test/box-tap/auth.test.lua
@@ -20,10 +20,7 @@ test:plan(42)
 local space = box.schema.space.create('tweedledum')
 local index = space:create_index('primary', { type = 'hash' })
 box.schema.user.create('test', {password='pass'})
-box.schema.user.grant('test', 'read,write,execute', 'universe')
 box.schema.user.create('test2', {password=''})
-box.schema.user.grant('test2', 'read,write,execute', 'universe')
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 
 -- check how authentication trigger work
 local msg, counter, succeeded
@@ -163,8 +160,6 @@ test:is(session.sync(), 0, "box.session.sync()")
 
 -- cleanup
 space:drop()
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
-box.schema.user.revoke('test', 'read,write,execute', 'universe')
 box.schema.user.drop('test', { if_exists = true})
 box.schema.user.drop("test2", { if_exists = true})
 
diff --git a/test/box-tap/session.test.lua b/test/box-tap/session.test.lua
index c3c07a67c..857bc643b 100755
--- a/test/box-tap/session.test.lua
+++ b/test/box-tap/session.test.lua
@@ -101,7 +101,8 @@ function audit_disconnect() box.space['tweedledum']:delete{session.id()} end
 test:is(type(session.on_connect(audit_connect)), "function", "type of trigger audit_connect on_connect")
 test:is(type(session.on_disconnect(audit_disconnect)), "function", "type of trigger audit_connect on_disconnect")
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.user.grant('guest', 'read,write', 'space', 'tweedledum')
+box.schema.user.grant('guest', 'execute', 'universe')
 a = net.box.connect(HOST, PORT)
 test:ok(a:eval('return space:get{box.session.id()}[1] == session.id()'), "eval get_id")
 test:ok(a:eval('return session.sync() ~= 0'), "eval sync")
@@ -112,12 +113,12 @@ session.on_connect(nil, audit_connect)
 session.on_disconnect(nil, audit_disconnect)
 test:is(active_connections, 0, "active connections after other triggers")
 
-space:drop()
+space:drop() -- tweedledum
 
 test:is(session.uid(), 1, "uid == 1")
 test:is(session.user(), "admin", "user is admin")
 test:is(session.sync(), 0, "sync constant")
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'universe')
 
 -- audit permission in on_connect/on_disconnect triggers
 box.schema.user.create('tester', { password = 'tester' })
@@ -199,7 +200,10 @@ function f2()
 	sync2 = box.session.sync()
 	cond:signal()
 end
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.func.create('f1')
+box.schema.func.create('f2')
+box.schema.user.grant('guest', 'execute', 'function', 'f1')
+box.schema.user.grant('guest', 'execute', 'function', 'f2')
 conn = net.box.connect(box.cfg.listen)
 test:ok(conn:ping(), 'connect to self')
 _ = fiber.create(function() conn:call('f1') end)
@@ -208,7 +212,8 @@ _ = fiber.create(function() conn:call('f2') end)
 while started ~= 2 do fiber.sleep(0.01) end
 test:isnt(sync1, sync2, 'session.sync() is request local')
 conn:close()
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'function', 'f1')
+box.schema.user.revoke('guest', 'execute', 'function', 'f2')
 
 inspector:cmd('stop server session with cleanup=1')
 session = nil
diff --git a/test/box/access.result b/test/box/access.result
index 9ea5568a5..f39678e26 100644
--- a/test/box/access.result
+++ b/test/box/access.result
@@ -370,7 +370,7 @@ box.schema.user.drop('uniuser')
 box.schema.user.create('grantor')
 ---
 ...
-box.schema.user.grant('grantor', 'read, write, execute', 'universe')
+box.schema.user.grant('grantor', 'read, write, execute, create, alter, drop', 'universe')
 ---
 ...
 session.su('grantor')
@@ -379,7 +379,7 @@ session.su('grantor')
 box.schema.user.create('grantee')
 ---
 ...
-box.schema.user.grant('grantee', 'read, write, execute', 'universe')
+box.schema.user.grant('grantee', 'read, write, execute, create, alter, drop', 'universe')
 ---
 - error: Grant access to universe '' is denied for user 'grantor'
 ...
diff --git a/test/box/access.test.lua b/test/box/access.test.lua
index 6ad7ee462..ee62df5fb 100644
--- a/test/box/access.test.lua
+++ b/test/box/access.test.lua
@@ -167,10 +167,10 @@ box.schema.user.drop('uniuser')
 -- only by its creator at the moment
 -- ------------------------------------------------------------
 box.schema.user.create('grantor')
-box.schema.user.grant('grantor', 'read, write, execute', 'universe')
+box.schema.user.grant('grantor', 'read, write, execute, create, alter, drop', 'universe')
 session.su('grantor')
 box.schema.user.create('grantee')
-box.schema.user.grant('grantee', 'read, write, execute', 'universe')
+box.schema.user.grant('grantee', 'read, write, execute, create, alter, drop', 'universe')
 session.su('grantee')
 -- fails - can't suicide - ask the creator to kill you
 box.schema.user.drop('grantee')
diff --git a/test/box/access_bin.result b/test/box/access_bin.result
index 7b30d11f2..df8ef8dee 100644
--- a/test/box/access_bin.result
+++ b/test/box/access_bin.result
@@ -8,7 +8,7 @@ test_run = env.new()
 -- Access control tests which require a binary protocol
 -- connection to the server
 --
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest','execute','universe')
 ---
 ...
 session = box.session
@@ -30,7 +30,7 @@ c:eval("return session.user()")
 c:close()
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'universe')
 ---
 ...
 -- gh-488 suid functions
diff --git a/test/box/access_bin.test.lua b/test/box/access_bin.test.lua
index 4c7a6d08f..e77d8c0a8 100644
--- a/test/box/access_bin.test.lua
+++ b/test/box/access_bin.test.lua
@@ -4,14 +4,14 @@ test_run = env.new()
 -- Access control tests which require a binary protocol
 -- connection to the server
 --
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest','execute','universe')
 session = box.session
 remote = require('net.box')
 c = remote.connect(box.cfg.listen)
 c:eval("session.su('admin')")
 c:eval("return session.user()")
 c:close()
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'universe')
 
 -- gh-488 suid functions
 --
diff --git a/test/box/access_escalation.result b/test/box/access_escalation.result
index a83f1ee8a..3991a7fab 100644
--- a/test/box/access_escalation.result
+++ b/test/box/access_escalation.result
@@ -84,7 +84,7 @@ box.schema.user.create('underprivileged')
 box.schema.user.grant('underprivileged', 'read,write', 'space', '_func')
 ---
 ...
-box.schema.user.grant('underprivileged', 'create', 'universe')
+box.schema.user.grant('underprivileged', 'create', 'function')
 ---
 ...
 box.session.su('underprivileged')
@@ -99,7 +99,16 @@ box.session.su('admin')
 --
 -- create a deprived function
 --
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.func.create('escalation')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'setuid')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'escalation')
+---
+...
+box.schema.user.grant('guest', 'read', 'space', '_space')
 ---
 ...
 connection = net:connect(os.getenv("LISTEN"))
@@ -119,7 +128,7 @@ fiber.cancel(background)
 box.schema.user.drop('underprivileged')
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.func.drop('escalation')
 ---
 ...
 connection:close()
diff --git a/test/box/access_escalation.test.lua b/test/box/access_escalation.test.lua
index 29b14c8ea..5aeed683d 100644
--- a/test/box/access_escalation.test.lua
+++ b/test/box/access_escalation.test.lua
@@ -61,7 +61,7 @@ connection:close()
 
 box.schema.user.create('underprivileged')
 box.schema.user.grant('underprivileged', 'read,write', 'space', '_func')
-box.schema.user.grant('underprivileged', 'create', 'universe')
+box.schema.user.grant('underprivileged', 'create', 'function')
 box.session.su('underprivileged')
 box.schema.func.create('setuid', {setuid=true})
 box.session.su('admin')
@@ -69,7 +69,10 @@ box.session.su('admin')
 -- create a deprived function
 --
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.func.create('escalation')
+box.schema.user.grant('guest', 'execute', 'function', 'setuid')
+box.schema.user.grant('guest', 'execute', 'function', 'escalation')
+box.schema.user.grant('guest', 'read', 'space', '_space')
 
 connection = net:connect(os.getenv("LISTEN"))
 
@@ -80,6 +83,5 @@ fiber.cancel(background)
 -- tear down
 
 box.schema.user.drop('underprivileged')
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
-
+box.schema.func.drop('escalation')
 connection:close()
diff --git a/test/box/access_misc.result b/test/box/access_misc.result
index 0f4892533..9291c413a 100644
--- a/test/box/access_misc.result
+++ b/test/box/access_misc.result
@@ -208,7 +208,7 @@ s:select()
 box.schema.user.create('uniuser')
 ---
 ...
-box.schema.user.grant('uniuser', 'read, write, execute', 'universe')
+box.schema.user.grant('uniuser', 'read, write, execute, create', 'universe')
 ---
 ...
 session.su('uniuser')
@@ -291,7 +291,7 @@ session.su('admin')
 box.schema.user.create('someuser')
 ---
 ...
-box.schema.user.grant('someuser', 'read, write, execute', 'universe')
+box.schema.user.grant('someuser', 'read, write, execute, create', 'universe')
 ---
 ...
 session.su('someuser')
diff --git a/test/box/access_misc.test.lua b/test/box/access_misc.test.lua
index 7dd796f62..b9fccb44e 100644
--- a/test/box/access_misc.test.lua
+++ b/test/box/access_misc.test.lua
@@ -86,7 +86,7 @@ s:select()
 -- and create this user session
 --
 box.schema.user.create('uniuser')
-box.schema.user.grant('uniuser', 'read, write, execute', 'universe')
+box.schema.user.grant('uniuser', 'read, write, execute, create', 'universe')
 session.su('uniuser')
 uid = session.uid()
 --
@@ -123,7 +123,7 @@ box.schema.func.create('uniuser_func')
 
 session.su('admin')
 box.schema.user.create('someuser')
-box.schema.user.grant('someuser', 'read, write, execute', 'universe')
+box.schema.user.grant('someuser', 'read, write, execute, create', 'universe')
 session.su('someuser')
 --
 -- Check drop objects of another user
diff --git a/test/box/call.result b/test/box/call.result
index 40d7ef952..349e5ce40 100644
--- a/test/box/call.result
+++ b/test/box/call.result
@@ -1,4 +1,4 @@
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.user.grant('guest', 'execute', 'universe')
 ---
 ...
 conn = require('net.box').connect(box.cfg.listen)
@@ -748,6 +748,6 @@ conn:close()
 require('msgpack').cfg { encode_sparse_safe = sparse_safe }
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'universe')
 ---
 ...
diff --git a/test/box/call.test.lua b/test/box/call.test.lua
index 0cc3b8fe6..a74aac4d1 100644
--- a/test/box/call.test.lua
+++ b/test/box/call.test.lua
@@ -1,4 +1,4 @@
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.user.grant('guest', 'execute', 'universe')
 
 conn = require('net.box').connect(box.cfg.listen)
 conn:ping()
@@ -240,4 +240,4 @@ conn:call_16("return_sparse4")
 conn:close()
 require('msgpack').cfg { encode_sparse_safe = sparse_safe }
 
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'universe')
diff --git a/test/box/errinj.result b/test/box/errinj.result
index 54b6d578f..d89c82247 100644
--- a/test/box/errinj.result
+++ b/test/box/errinj.result
@@ -784,7 +784,7 @@ test_run:cmd('setopt delimiter ""');
 - true
 ...
 -- Port_dump can fail.
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.user.grant('guest', 'read', 'space', '_space')
 ---
 ...
 cn = net_box.connect(box.cfg.listen)
@@ -816,7 +816,7 @@ errinj.set('ERRINJ_PORT_DUMP', false)
 cn:close()
 ---
 ...
-box.schema.user.revoke('guest', 'read, write, execute', 'universe')
+box.schema.user.revoke('guest', 'read', 'space', '_space')
 ---
 ...
 run()
@@ -1069,15 +1069,15 @@ s:drop()
 -- gh-3255: iproto can crash and discard responses, if a network
 -- is saturated, and DML yields too long on commit.
 --
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 s = box.schema.space.create('test')
 ---
 ...
 _ = s:create_index('pk')
 ---
 ...
+box.schema.user.grant('guest', 'read,write,alter', 'space', 'test')
+---
+...
 c = net_box.connect(box.cfg.listen)
 ---
 ...
@@ -1108,12 +1108,21 @@ s:drop()
 -- gh-3325: do not cancel already sent requests, when a schema
 -- change is detected.
 --
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
 s = box.schema.create_space('test')
 ---
 ...
 pk = s:create_index('pk')
 ---
 ...
+box.schema.user.grant('guest', 'read,write,alter', 'space', 'test')
+---
+...
+box.schema.user.grant('guest', 'write', 'space', '_index')
+---
+...
 s:replace{1, 1}
 ---
 - [1, 1]
@@ -1164,6 +1173,9 @@ cn:close()
 s:drop()
 ---
 ...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
 --
 -- If message memory pool is used up, stop the connection, until
 -- the pool has free memory.
@@ -1189,6 +1201,12 @@ function long_poll_f()
 end;
 ---
 ...
+box.schema.func.create('long_poll_f');
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'long_poll_f');
+---
+...
 test_run:cmd('setopt delimiter ""');
 ---
 - true
@@ -1250,7 +1268,7 @@ while finished ~= 2 do fiber.sleep(0.01) end
 cn:close()
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute','universe')
+box.schema.user.revoke('guest', 'execute', 'function', 'long_poll_f')
 ---
 ...
 --
diff --git a/test/box/errinj.test.lua b/test/box/errinj.test.lua
index 185ae19fc..5046159b5 100644
--- a/test/box/errinj.test.lua
+++ b/test/box/errinj.test.lua
@@ -245,7 +245,7 @@ test_run:cmd('setopt delimiter ""');
 
 -- Port_dump can fail.
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.user.grant('guest', 'read', 'space', '_space')
 
 cn = net_box.connect(box.cfg.listen)
 cn:ping()
@@ -255,7 +255,7 @@ assert(not ok)
 assert(string.match(tostring(ret), 'Failed to allocate'))
 errinj.set('ERRINJ_PORT_DUMP', false)
 cn:close()
-box.schema.user.revoke('guest', 'read, write, execute', 'universe')
+box.schema.user.revoke('guest', 'read', 'space', '_space')
 
 run()
 ch:get()
@@ -350,10 +350,9 @@ s:drop()
 -- is saturated, and DML yields too long on commit.
 --
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 s = box.schema.space.create('test')
 _ = s:create_index('pk')
-
+box.schema.user.grant('guest', 'read,write,alter', 'space', 'test')
 c = net_box.connect(box.cfg.listen)
 
 ch = fiber.channel(200)
@@ -369,8 +368,14 @@ s:drop()
 -- gh-3325: do not cancel already sent requests, when a schema
 -- change is detected.
 --
+
+box.schema.user.grant('guest', 'execute', 'universe')
+
 s = box.schema.create_space('test')
 pk = s:create_index('pk')
+
+box.schema.user.grant('guest', 'read,write,alter', 'space', 'test')
+box.schema.user.grant('guest', 'write', 'space', '_index')
 s:replace{1, 1}
 cn = net_box.connect(box.cfg.listen)
 errinj.set("ERRINJ_WAL_DELAY", true)
@@ -388,6 +393,7 @@ while ok == nil do fiber.sleep(0.01) end
 ok, err
 cn:close()
 s:drop()
+box.schema.user.revoke('guest', 'execute', 'universe')
 
 --
 -- If message memory pool is used up, stop the connection, until
@@ -403,6 +409,10 @@ function long_poll_f()
     while not continue do fiber.sleep(0.01) end
     finished = finished + 1
 end;
+
+box.schema.func.create('long_poll_f');
+box.schema.user.grant('guest', 'execute', 'function', 'long_poll_f');
+
 test_run:cmd('setopt delimiter ""');
 cn = net_box.connect(box.cfg.listen)
 function long_poll() cn:call('long_poll_f') end
@@ -428,8 +438,7 @@ errinj.set("ERRINJ_TESTING", false)
 while finished ~= 2 do fiber.sleep(0.01) end
 cn:close()
 
-box.schema.user.revoke('guest', 'read,write,execute','universe')
-
+box.schema.user.revoke('guest', 'execute', 'function', 'long_poll_f')
 --
 -- gh-3289: drop/truncate leaves the space in inconsistent
 -- state if WAL write fails.
diff --git a/test/box/net.box.result b/test/box/net.box.result
index d521534fe..2cfddba72 100644
--- a/test/box/net.box.result
+++ b/test/box/net.box.result
@@ -203,6 +203,9 @@ remote.self:eval('!invalid expression')
 ---
 - error: '[string "return !invalid expression"]:1: unexpected symbol near ''!'''
 ...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
 --
 -- gh-822: net.box.call should roll back local transaction on error
 --
@@ -265,10 +268,10 @@ test_run:cmd("setopt delimiter ''");
 box.space.gh822:drop()
 ---
 ...
-box.schema.user.revoke('guest','execute','universe')
+box.schema.user.grant('guest', 'read,write', 'space', 'net_box_test_space')
 ---
 ...
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest', 'execute', 'universe')
 ---
 ...
 cn:close()
@@ -496,6 +499,15 @@ cn.space.net_box_test_space:get(354)
 - [354, 1, 2, 4]
 ...
 -- reconnects after errors
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
+box.schema.func.create('test_foo')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'test_foo')
+---
+...
 -- -- 1. no reconnect
 x_fatal(cn)
 ---
@@ -593,6 +605,12 @@ type(fiber.create(function() fiber.sleep(.5) x_fatal(cn) end))
 function pause() fiber.sleep(10) return true end
 ---
 ...
+box.schema.func.create('pause')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'pause')
+---
+...
 cn:call('pause')
 ---
 - error: Peer closed
@@ -601,6 +619,9 @@ cn:call('test_foo', {'a', 'b', 'c'})
 ---
 - [[{'a': 1}], [{'b': 2}], 'c']
 ...
+box.schema.func.drop('pause')
+---
+...
 -- call
 remote.self:call('test_foo', {'a', 'b', 'c'})
 ---
@@ -612,6 +633,15 @@ cn:call('test_foo', {'a', 'b', 'c'})
 ---
 - [[{'a': 1}], [{'b': 2}], 'c']
 ...
+box.schema.func.drop('test_foo')
+---
+...
+box.schema.func.create('long_rep')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'long_rep')
+---
+...
 -- long replies
 function long_rep() return { 1,  string.rep('a', 5000) } end
 ---
@@ -641,6 +671,9 @@ res[2] == string.rep('a', 50000)
 ---
 - true
 ...
+box.schema.func.drop('long_rep')
+---
+...
 -- a.b.c.d
 u = '84F7BCFA-079C-46CC-98B4-F0C821BE833E'
 ---
@@ -654,6 +687,15 @@ X.X = X
 function X.fn(x,y) return y or x end
 ---
 ...
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
+cn:close()
+---
+...
+cn = remote.connect(LISTEN.host, LISTEN.service)
+---
+...
 cn:call('X.fn', {u})
 ---
 - 84F7BCFA-079C-46CC-98B4-F0C821BE833E
@@ -666,6 +708,12 @@ cn:call('X.X.X.X:fn', {u})
 ---
 - 84F7BCFA-079C-46CC-98B4-F0C821BE833E
 ...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
+cn:close()
+---
+...
 -- auth
 cn = remote.connect(LISTEN.host, LISTEN.service, { user = 'netbox', password = '123', wait_connected = true })
 ---
@@ -685,7 +733,10 @@ cn.state
 box.schema.user.create('netbox', { password  = 'test' })
 ---
 ...
-box.schema.user.grant('netbox', 'read, write, execute', 'universe');
+box.schema.user.grant('netbox', 'read,write', 'space', 'net_box_test_space')
+---
+...
+box.schema.user.grant('netbox', 'execute', 'universe')
 ---
 ...
 cn = remote.connect(LISTEN.host, LISTEN.service, { user = 'netbox', password = 'test' })
@@ -1089,6 +1140,15 @@ cn:close()
 cn = remote.connect(LISTEN.host, LISTEN.service)
 ---
 ...
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
+cn:close()
+---
+...
+cn = remote.connect(LISTEN.host, LISTEN.service)
+---
+...
 cn:eval('return true')
 ---
 - true
@@ -1112,6 +1172,9 @@ remote.self.eval('return true')
 ---
 - error: 'Use remote:eval(...) instead of remote.eval(...):'
 ...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
 -- uri as the first argument
 uri = string.format('%s:%s@%s:%s', 'netbox', 'test', LISTEN.host, LISTEN.service)
 ---
@@ -1156,13 +1219,16 @@ cn:ping()
 cn:close()
 ---
 ...
-box.schema.user.revoke('netbox', 'read, write, execute', 'universe');
----
-...
 box.schema.user.drop('netbox')
 ---
 ...
 -- #594: bad argument #1 to 'setmetatable' (table expected, got number)
+box.schema.func.create('dostring')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'dostring')
+---
+...
 test_run:cmd("setopt delimiter ';'")
 ---
 - true
@@ -1182,6 +1248,9 @@ test_run:cmd("setopt delimiter ''");
 gh594()
 ---
 ...
+box.schema.func.drop('dostring')
+---
+...
 -- #636: Reload schema on demand
 sp = box.schema.space.create('test_old')
 ---
@@ -1193,6 +1262,9 @@ sp:insert{1, 2, 3}
 ---
 - [1, 2, 3]
 ...
+box.schema.user.grant('guest', 'read', 'space', 'test_old')
+---
+...
 con = remote.new(box.cfg.listen)
 ---
 ...
@@ -1218,6 +1290,9 @@ sp:insert{2, 3, 4}
 ---
 - [2, 3, 4]
 ...
+box.schema.user.grant('guest', 'read', 'space', 'test')
+---
+...
 con.space.test == nil
 ---
 - true
@@ -1248,6 +1323,9 @@ file_log:seek(0, 'SEEK_END') ~= 0
 ---
 - true
 ...
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
 test_run:cmd("setopt delimiter ';'")
 ---
 - true
@@ -1269,6 +1347,9 @@ test_run:grep_log("default", "ER_NO_SUCH_PROC")
 ---
 - ER_NO_SUCH_PROC
 ...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
 -- gh-983 selecting a lot of data crashes the server or hangs the
 -- connection
 -- gh-983 test case: iproto connection selecting a lot of data
@@ -1284,6 +1365,9 @@ data1k = "aaaabbbbccccddddeeeeffffgggghhhhaaaabbbbccccddddeeeeffffgggghhhhaaaabb
 for i = 0,10000 do box.space.test:insert{i, data1k} end
 ---
 ...
+box.schema.user.grant('guest', 'read', 'space', 'test')
+---
+...
 net = require('net.box')
 ---
 ...
@@ -1309,6 +1393,9 @@ _ = box.space.test:create_index('covering', {type = 'TREE', parts = {1,'unsigned
 _ = box.space.test:insert{1, 2, "string"}
 ---
 ...
+box.schema.user.grant('guest', 'read,write', 'space', 'test')
+---
+...
 c = net:connect(box.cfg.listen)
 ---
 ...
@@ -1365,6 +1452,9 @@ box.space.test:drop()
 function echo(...) return ... end
 ---
 ...
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
 c = net.connect(box.cfg.listen)
 ---
 ...
@@ -1404,6 +1494,9 @@ c:eval('return echo(...)', 42)
 c:close()
 ---
 ...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
 --
 -- gh-2195 export pure msgpack from net.box
 --
@@ -1413,6 +1506,12 @@ space = box.schema.space.create('test')
 _ = box.space.test:create_index('primary')
 ---
 ...
+box.schema.user.grant('guest', 'read,write', 'space', 'test')
+---
+...
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
 c = net.connect(box.cfg.listen)
 ---
 ...
@@ -1648,6 +1747,9 @@ c:close()
 space:drop()
 ---
 ...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
 -- gh-1904 net.box hangs in :close() if a fiber was cancelled
 -- while blocked in :_wait_state() in :_request()
 options = {user = 'netbox', password = 'badpass', wait_connected = false, reconnect_after = 0.01}
@@ -1665,6 +1767,9 @@ fiber.sleep(0.01)
 f:cancel(); c:close()
 ---
 ...
+box.schema.user.grant('guest', 'read', 'space', '_schema')
+---
+...
 -- check for on_schema_reload callback
 test_run:cmd("setopt delimiter ';'")
 ---
@@ -1717,7 +1822,7 @@ test_run:cmd("setopt delimiter ''");
 ---
 - true
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'read', 'space', '_schema')
 ---
 ...
 -- Tarantool < 1.7.1 compatibility (gh-1533)
@@ -1806,7 +1911,7 @@ space ~= nil
 _ = box.space.test:create_index('primary')
 ---
 ...
-box.schema.user.grant('guest','read,write,execute','space', 'test')
+box.schema.user.grant('guest', 'read', 'space', 'test')
 ---
 ...
 c = net.connect(box.cfg.listen)
@@ -1885,7 +1990,7 @@ c:close()
 --
 -- gh-2642: box.session.type()
 --
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest','execute','universe')
 ---
 ...
 c = net.connect(box.cfg.listen)
@@ -1898,6 +2003,9 @@ c:call("box.session.type")
 c:close()
 ---
 ...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
 --
 -- On_connect/disconnect triggers.
 --
@@ -1977,6 +2085,9 @@ space:drop()
 space = box.schema.space.create('test')
 ---
 ...
+box.schema.user.grant('guest', 'read', 'space', 'test')
+---
+...
 c = net.connect(box.cfg.listen)
 ---
 ...
@@ -2010,16 +2121,28 @@ c.space.test.index.test_index ~= nil
 - true
 ...
 -- cleanup
-box.schema.user.revoke('guest','read,write,execute','universe')
----
-...
 space:drop()
 ---
 ...
 --
 -- gh-946: long polling CALL blocks input
 --
-box.schema.user.grant('guest', 'execute', 'universe')
+box.schema.func.create('fast_call')
+---
+...
+box.schema.func.create('long_call')
+---
+...
+box.schema.func.create('wait_signal')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'fast_call')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'long_call')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'wait_signal')
 ---
 ...
 c = net.connect(box.cfg.listen)
@@ -2136,6 +2259,15 @@ disconnected -- true
 box.session.on_disconnect(nil, on_disconnect)
 ---
 ...
+box.schema.func.drop('long_call')
+---
+...
+box.schema.func.drop('fast_call')
+---
+...
+box.schema.func.drop('wait_signal')
+---
+...
 --
 -- gh-2666: check that netbox.call is not repeated on schema
 -- change.
@@ -2155,6 +2287,12 @@ count = 0
 function create_space(name) count = count + 1 box.schema.create_space(name) return true end
 ---
 ...
+box.schema.func.create('create_space')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'create_space')
+---
+...
 c = net.connect(box.cfg.listen)
 ---
 ...
@@ -2203,6 +2341,9 @@ box.schema.user.revoke('guest', 'create', 'universe')
 c:close()
 ---
 ...
+box.schema.func.drop('create_space')
+---
+...
 --
 -- gh-3164: netbox connection is not closed and garbage collected
 -- ever, if reconnect_after is set.
@@ -2311,7 +2452,7 @@ weak.c
 -- binary or text protocol, and netbox could not be created from
 -- existing socket.
 --
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest', 'execute', 'universe')
 ---
 ...
 urilib = require('uri')
@@ -2350,6 +2491,9 @@ c:call('kek', {300})
 s = box.schema.create_space('test')
 ---
 ...
+box.schema.user.grant('guest', 'read,write', 'space', 'test')
+---
+...
 pk = s:create_index('pk')
 ---
 ...
@@ -2404,7 +2548,7 @@ while c.state ~= 'error_reconnect' do fiber.sleep(0.01) end
 c:close()
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'universe')
 ---
 ...
 c.state
@@ -2420,7 +2564,7 @@ c = nil
 space = box.schema.create_space('test')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.user.grant('guest', 'read', 'space', 'test')
 ---
 ...
 _ = space:create_index('pk')
@@ -2444,6 +2588,15 @@ space:drop()
 space = box.schema.create_space('test')
 ---
 ...
+c:close()
+---
+...
+box.schema.user.grant('guest', 'read', 'space', 'test')
+---
+...
+c = net:connect(box.cfg.listen)
+---
+...
 box.internal.collation.create('test', 'ICU', 'ru-RU')
 ---
 ...
@@ -2482,6 +2635,12 @@ c = nil
 cond = nil
 ---
 ...
+box.schema.func.create('long_function')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'long_function')
+---
+...
 function long_function(...) cond = fiber.cond() cond:wait() return ... end
 ---
 ...
@@ -2589,6 +2748,15 @@ ret
 ---
 - [1, 2, 3]
 ...
+c:close()
+---
+...
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
+c = net:connect(box.cfg.listen)
+---
+...
 future = c:eval('return long_function(...)', {1, 2, 3}, {is_async = true})
 ---
 ...
@@ -2609,6 +2777,15 @@ future:wait_result(100)
 ---
 - [1, 2, 3]
 ...
+c:close()
+---
+...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
+c = net:connect(box.cfg.listen)
+---
+...
 --
 -- Ensure the request is garbage collected both if is not used and
 -- if is.
@@ -2716,6 +2893,15 @@ ret
 --
 -- Test space methods.
 --
+c:close()
+---
+...
+box.schema.user.grant('guest', 'read,write', 'space', 'test')
+---
+...
+c = net:connect(box.cfg.listen)
+---
+...
 future = c.space.test:select({1}, {is_async = true})
 ---
 ...
@@ -2822,6 +3008,15 @@ future:wait_result(100)
 ---
 - [5, 6]
 ...
+c:close()
+---
+...
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
+c = net:connect(box.cfg.listen)
+---
+...
 future = c.space.test.index.pk:count({3}, {is_async = true})
 ---
 ...
@@ -2829,6 +3024,15 @@ future:wait_result(100)
 ---
 - 1
 ...
+c:close()
+---
+...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
+c = net:connect(box.cfg.listen)
+---
+...
 future = c.space.test.index.pk:delete({3}, {is_async = true})
 ---
 ...
@@ -2997,12 +3201,30 @@ result
 ---
 - {48: [1, 2, 3]}
 ...
+box.schema.func.drop('long_function')
+---
+...
 --
 -- Test async schema version change.
 --
 function change_schema(i) local tmp = box.schema.create_space('test'..i) return 'ok' end
 ---
 ...
+box.schema.func.create('change_schema')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'change_schema')
+---
+...
+box.schema.user.grant('guest', 'write', 'space', '_schema')
+---
+...
+box.schema.user.grant('guest', 'read,write', 'space', '_space')
+---
+...
+box.schema.user.grant('guest', 'create', 'space')
+---
+...
 future1 = c:call('change_schema', {'1'}, {is_async = true})
 ---
 ...
@@ -3039,6 +3261,9 @@ box.space.test2:drop()
 box.space.test3:drop()
 ---
 ...
+box.schema.func.drop('change_schema')
+---
+...
 --
 -- gh-3400: long-poll input discard must not touch event loop of
 -- a closed connection.
@@ -3090,6 +3315,12 @@ test_run:grep_log('default', 'too big packet size in the header') ~= nil
 ---
 - true
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'write', 'space', '_schema')
+---
+...
+box.schema.user.revoke('guest', 'read,write', 'space', '_space')
+---
+...
+box.schema.user.revoke('guest', 'create', 'space')
 ---
 ...
diff --git a/test/box/net.box.test.lua b/test/box/net.box.test.lua
index 623475d41..bf575c6bc 100644
--- a/test/box/net.box.test.lua
+++ b/test/box/net.box.test.lua
@@ -31,7 +31,6 @@ log.info("ping is done")
 
 cn:ping()
 
-
 -- check permissions
 cn:call('unexists_procedure')
 function test_foo(a,b,c) return { {{ [a] = 1 }}, {{ [b] = 2 }}, c } end
@@ -73,6 +72,8 @@ remote.self:eval('error("exception")')
 remote.self:eval('box.error(0)')
 remote.self:eval('!invalid expression')
 
+box.schema.user.revoke('guest', 'execute', 'universe')
+
 --
 -- gh-822: net.box.call should roll back local transaction on error
 --
@@ -113,8 +114,9 @@ rollback_on_eval_error();
 test_run:cmd("setopt delimiter ''");
 box.space.gh822:drop()
 
-box.schema.user.revoke('guest','execute','universe')
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest', 'read,write', 'space', 'net_box_test_space')
+box.schema.user.grant('guest', 'execute', 'universe')
+
 cn:close()
 cn = remote.connect(box.cfg.listen)
 
@@ -189,6 +191,10 @@ cn.space.net_box_test_space:get(354)
 
 -- reconnects after errors
 
+box.schema.user.revoke('guest', 'execute', 'universe')
+box.schema.func.create('test_foo')
+box.schema.user.grant('guest', 'execute', 'function', 'test_foo')
+
 -- -- 1. no reconnect
 x_fatal(cn)
 cn.state
@@ -224,13 +230,19 @@ cn1:close()
 type(fiber.create(function() fiber.sleep(.5) x_fatal(cn) end))
 function pause() fiber.sleep(10) return true end
 
+box.schema.func.create('pause')
+box.schema.user.grant('guest', 'execute', 'function', 'pause')
 cn:call('pause')
 cn:call('test_foo', {'a', 'b', 'c'})
-
+box.schema.func.drop('pause')
 
 -- call
 remote.self:call('test_foo', {'a', 'b', 'c'})
 cn:call('test_foo', {'a', 'b', 'c'})
+box.schema.func.drop('test_foo')
+
+box.schema.func.create('long_rep')
+box.schema.user.grant('guest', 'execute', 'function', 'long_rep')
 
 -- long replies
 function long_rep() return { 1,  string.rep('a', 5000) } end
@@ -243,14 +255,21 @@ res = cn:call('long_rep')
 res[1] == 1
 res[2] == string.rep('a', 50000)
 
+box.schema.func.drop('long_rep')
+
 -- a.b.c.d
 u = '84F7BCFA-079C-46CC-98B4-F0C821BE833E'
 X = {}
 X.X = X
 function X.fn(x,y) return y or x end
+box.schema.user.grant('guest', 'execute', 'universe')
+cn:close()
+cn = remote.connect(LISTEN.host, LISTEN.service)
 cn:call('X.fn', {u})
 cn:call('X.X.X.X.X.X.X.fn', {u})
 cn:call('X.X.X.X:fn', {u})
+box.schema.user.revoke('guest', 'execute', 'universe')
+cn:close()
 
 -- auth
 
@@ -259,9 +278,10 @@ cn:is_connected()
 cn.error
 cn.state
 
-box.schema.user.create('netbox', { password  = 'test' })
-box.schema.user.grant('netbox', 'read, write, execute', 'universe');
 
+box.schema.user.create('netbox', { password  = 'test' })
+box.schema.user.grant('netbox', 'read,write', 'space', 'net_box_test_space')
+box.schema.user.grant('netbox', 'execute', 'universe')
 cn = remote.connect(LISTEN.host, LISTEN.service, { user = 'netbox', password = 'test' })
 cn.state
 cn.error
@@ -404,6 +424,9 @@ cn:close()
 -- #544 usage for remote[point]method
 cn = remote.connect(LISTEN.host, LISTEN.service)
 
+box.schema.user.grant('guest', 'execute', 'universe')
+cn:close()
+cn = remote.connect(LISTEN.host, LISTEN.service)
 cn:eval('return true')
 cn.eval('return true')
 
@@ -413,7 +436,7 @@ cn:close()
 
 remote.self:eval('return true')
 remote.self.eval('return true')
-
+box.schema.user.revoke('guest', 'execute', 'universe')
 
 -- uri as the first argument
 uri = string.format('%s:%s@%s:%s', 'netbox', 'test', LISTEN.host, LISTEN.service)
@@ -432,10 +455,11 @@ cn = remote.new(uri, { user = 'netbox', password = 'test' })
 cn:ping()
 cn:close()
 
-box.schema.user.revoke('netbox', 'read, write, execute', 'universe');
 box.schema.user.drop('netbox')
 
 -- #594: bad argument #1 to 'setmetatable' (table expected, got number)
+box.schema.func.create('dostring')
+box.schema.user.grant('guest', 'execute', 'function', 'dostring')
 test_run:cmd("setopt delimiter ';'")
 function gh594()
     local cn = remote.connect(box.cfg.listen)
@@ -445,12 +469,15 @@ function gh594()
 end;
 test_run:cmd("setopt delimiter ''");
 gh594()
+box.schema.func.drop('dostring')
+
 
 -- #636: Reload schema on demand
 sp = box.schema.space.create('test_old')
 _ = sp:create_index('primary')
 sp:insert{1, 2, 3}
 
+box.schema.user.grant('guest', 'read', 'space', 'test_old')
 con = remote.new(box.cfg.listen)
 con:ping()
 con.space.test_old:select{}
@@ -460,6 +487,8 @@ sp = box.schema.space.create('test')
 _ = sp:create_index('primary')
 sp:insert{2, 3, 4}
 
+box.schema.user.grant('guest', 'read', 'space', 'test')
+
 con.space.test == nil
 con:reload_schema()
 con.space.test:select{}
@@ -472,6 +501,7 @@ name = string.match(arg[0], "([^,]+)%.lua")
 file_log = require('fio').open(name .. '.log', {'O_RDONLY', 'O_NONBLOCK'})
 file_log:seek(0, 'SEEK_END') ~= 0
 
+box.schema.user.grant('guest', 'execute', 'universe')
 test_run:cmd("setopt delimiter ';'")
 
 _ = fiber.create(
@@ -483,6 +513,7 @@ _ = fiber.create(
 );
 test_run:cmd("setopt delimiter ''");
 test_run:grep_log("default", "ER_NO_SUCH_PROC")
+box.schema.user.revoke('guest', 'execute', 'universe')
 
 -- gh-983 selecting a lot of data crashes the server or hangs the
 -- connection
@@ -495,6 +526,7 @@ data1k = "aaaabbbbccccddddeeeeffffgggghhhhaaaabbbbccccddddeeeeffffgggghhhhaaaabb
 
 for i = 0,10000 do box.space.test:insert{i, data1k} end
 
+box.schema.user.grant('guest', 'read', 'space', 'test')
 net = require('net.box')
 c = net:connect(box.cfg.listen)
 r = c.space.test:select(nil, {limit=5000})
@@ -505,6 +537,7 @@ _ = box.schema.space.create('test')
 _ = box.space.test:create_index('primary', {type = 'TREE', parts = {1,'unsigned'}})
 _ = box.space.test:create_index('covering', {type = 'TREE', parts = {1,'unsigned',3,'string',2,'unsigned'}})
 _ = box.space.test:insert{1, 2, "string"}
+box.schema.user.grant('guest', 'read,write', 'space', 'test')
 c = net:connect(box.cfg.listen)
 c.space.test:select{}
 c.space.test:upsert({1, 2, 'nothing'}, {{'+', 2, 1}}) -- common update
@@ -522,6 +555,7 @@ box.space.test:drop()
 
 -- CALL vs CALL_16 in connect options
 function echo(...) return ... end
+box.schema.user.grant('guest', 'execute', 'universe')
 c = net.connect(box.cfg.listen)
 c:call('echo', {42})
 c:eval('return echo(...)', {42})
@@ -533,6 +567,7 @@ c = net.connect(box.cfg.listen, {call_16 = true})
 c:call('echo', 42)
 c:eval('return echo(...)', 42)
 c:close()
+box.schema.user.revoke('guest', 'execute', 'universe')
 
 --
 -- gh-2195 export pure msgpack from net.box
@@ -540,6 +575,8 @@ c:close()
 
 space = box.schema.space.create('test')
 _ = box.space.test:create_index('primary')
+box.schema.user.grant('guest', 'read,write', 'space', 'test')
+box.schema.user.grant('guest', 'execute', 'universe')
 c = net.connect(box.cfg.listen)
 ibuf = require('buffer').ibuf()
 
@@ -626,6 +663,7 @@ ibuf.rpos == rpos, ibuf.wpos == wpos
 ibuf = nil
 c:close()
 space:drop()
+box.schema.user.revoke('guest', 'execute', 'universe')
 
 -- gh-1904 net.box hangs in :close() if a fiber was cancelled
 -- while blocked in :_wait_state() in :_request()
@@ -635,6 +673,8 @@ f = fiber.create(function() c:call("") end)
 fiber.sleep(0.01)
 f:cancel(); c:close()
 
+box.schema.user.grant('guest', 'read', 'space', '_schema')
+
 -- check for on_schema_reload callback
 test_run:cmd("setopt delimiter ';'")
 do
@@ -676,7 +716,7 @@ do
 end;
 test_run:cmd("setopt delimiter ''");
 
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'read', 'space', '_schema')
 
 -- Tarantool < 1.7.1 compatibility (gh-1533)
 c = net.new(box.cfg.listen)
@@ -722,7 +762,7 @@ test_run:cmd("clear filter")
 space = box.schema.space.create('test', {format={{name="id", type="unsigned"}}})
 space ~= nil
 _ = box.space.test:create_index('primary')
-box.schema.user.grant('guest','read,write,execute','space', 'test')
+box.schema.user.grant('guest', 'read', 'space', 'test')
 
 c = net.connect(box.cfg.listen)
 
@@ -762,10 +802,12 @@ c:close()
 -- gh-2642: box.session.type()
 --
 
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest','execute','universe')
 c = net.connect(box.cfg.listen)
 c:call("box.session.type")
 c:close()
+box.schema.user.revoke('guest', 'execute', 'universe')
+
 
 --
 -- On_connect/disconnect triggers.
@@ -795,6 +837,7 @@ test_run:cmd('stop server connecter')
 --
 space:drop()
 space = box.schema.space.create('test')
+box.schema.user.grant('guest', 'read', 'space', 'test')
 c = net.connect(box.cfg.listen)
 cspace = c.space.test
 space.index.test_index == nil
@@ -806,15 +849,18 @@ cspace.index.test_index ~= nil
 c.space.test.index.test_index ~= nil
 
 -- cleanup
-box.schema.user.revoke('guest','read,write,execute','universe')
 
 space:drop()
 
 --
 -- gh-946: long polling CALL blocks input
 --
-box.schema.user.grant('guest', 'execute', 'universe')
-
+box.schema.func.create('fast_call')
+box.schema.func.create('long_call')
+box.schema.func.create('wait_signal')
+box.schema.user.grant('guest', 'execute', 'function', 'fast_call')
+box.schema.user.grant('guest', 'execute', 'function', 'long_call')
+box.schema.user.grant('guest', 'execute', 'function', 'wait_signal')
 c = net.connect(box.cfg.listen)
 
 N = 100
@@ -871,6 +917,9 @@ disconnected -- true
 
 box.session.on_disconnect(nil, on_disconnect)
 
+box.schema.func.drop('long_call')
+box.schema.func.drop('fast_call')
+box.schema.func.drop('wait_signal')
 --
 -- gh-2666: check that netbox.call is not repeated on schema
 -- change.
@@ -880,6 +929,8 @@ box.schema.user.grant('guest', 'write', 'space', '_schema')
 box.schema.user.grant('guest', 'create', 'universe')
 count = 0
 function create_space(name) count = count + 1 box.schema.create_space(name) return true end
+box.schema.func.create('create_space')
+box.schema.user.grant('guest', 'execute', 'function', 'create_space')
 c = net.connect(box.cfg.listen)
 c:call('create_space', {'test1'})
 count
@@ -894,6 +945,7 @@ box.schema.user.revoke('guest', 'write', 'space', '_space')
 box.schema.user.revoke('guest', 'write', 'space', '_schema')
 box.schema.user.revoke('guest', 'create', 'universe')
 c:close()
+box.schema.func.drop('create_space')
 
 --
 -- gh-3164: netbox connection is not closed and garbage collected
@@ -946,7 +998,7 @@ weak.c
 -- binary or text protocol, and netbox could not be created from
 -- existing socket.
 --
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest', 'execute', 'universe')
 urilib = require('uri')
 uri = urilib.parse(tostring(box.cfg.listen))
 s, greeting = net.establish_connection(uri.host, uri.service)
@@ -959,6 +1011,7 @@ c:eval('a = 200')
 a
 c:call('kek', {300})
 s = box.schema.create_space('test')
+box.schema.user.grant('guest', 'read,write', 'space', 'test')
 pk = s:create_index('pk')
 c:reload_schema()
 c.space.test:replace{1}
@@ -983,7 +1036,7 @@ c = net.connect('localhost:33333', {reconnect_after = 0.1, wait_connected = fals
 while c.state ~= 'error_reconnect' do fiber.sleep(0.01) end
 c:close()
 
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'universe')
 c.state
 c = nil
 
@@ -991,7 +1044,7 @@ c = nil
 -- gh-3256 net.box is_nullable and collation options output
 --
 space = box.schema.create_space('test')
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.user.grant('guest', 'read', 'space', 'test')
 _ = space:create_index('pk')
 _ = space:create_index('sk', {parts = {{2, 'unsigned', is_nullable = true}}})
 c = net:connect(box.cfg.listen)
@@ -999,6 +1052,9 @@ c.space.test.index.sk.parts
 space:drop()
 
 space = box.schema.create_space('test')
+c:close()
+box.schema.user.grant('guest', 'read', 'space', 'test')
+c = net:connect(box.cfg.listen)
 box.internal.collation.create('test', 'ICU', 'ru-RU')
 _ = space:create_index('sk', { type = 'tree', parts = {{1, 'str', collation = 'test'}}, unique = true })
 c:reload_schema()
@@ -1013,6 +1069,8 @@ c = nil
 -- gh-3107: fiber-async netbox.
 --
 cond = nil
+box.schema.func.create('long_function')
+box.schema.user.grant('guest', 'execute', 'function', 'long_function')
 function long_function(...) cond = fiber.cond() cond:wait() return ... end
 function finalize_long() while not cond do fiber.sleep(0.01) end cond:signal() cond = nil end
 s = box.schema.create_space('test')
@@ -1050,13 +1108,19 @@ _ = fiber.create(function() ret = c:call('long_function', {1, 2, 3}, {is_async =
 finalize_long()
 while not ret do fiber.sleep(0.01) end
 ret
-
+c:close()
+box.schema.user.grant('guest', 'execute', 'universe')
+c = net:connect(box.cfg.listen)
 future = c:eval('return long_function(...)', {1, 2, 3}, {is_async = true})
 future:result()
 future:wait_result(0.01) -- Must fail on timeout.
 finalize_long()
 future:wait_result(100)
 
+c:close()
+box.schema.user.revoke('guest', 'execute', 'universe')
+c = net:connect(box.cfg.listen)
+
 --
 -- Ensure the request is garbage collected both if is not used and
 -- if is.
@@ -1095,6 +1159,9 @@ ret
 --
 -- Test space methods.
 --
+c:close()
+box.schema.user.grant('guest', 'read,write', 'space', 'test')
+c = net:connect(box.cfg.listen)
 future = c.space.test:select({1}, {is_async = true})
 ret = future:wait_result(100)
 ret
@@ -1128,8 +1195,14 @@ future = c.space.test.index.pk:min({}, {is_async = true})
 future:wait_result(100)
 future = c.space.test.index.pk:max({}, {is_async = true})
 future:wait_result(100)
+c:close()
+box.schema.user.grant('guest', 'execute', 'universe')
+c = net:connect(box.cfg.listen)
 future = c.space.test.index.pk:count({3}, {is_async = true})
 future:wait_result(100)
+c:close()
+box.schema.user.revoke('guest', 'execute', 'universe')
+c = net:connect(box.cfg.listen)
 future = c.space.test.index.pk:delete({3}, {is_async = true})
 future:wait_result(100)
 s:get{3}
@@ -1194,10 +1267,17 @@ future:wait_result(100)
 result, ibuf.rpos = msgpack.decode_unchecked(ibuf.rpos)
 result
 
+box.schema.func.drop('long_function')
+
 --
 -- Test async schema version change.
 --
 function change_schema(i) local tmp = box.schema.create_space('test'..i) return 'ok' end
+box.schema.func.create('change_schema')
+box.schema.user.grant('guest', 'execute', 'function', 'change_schema')
+box.schema.user.grant('guest', 'write', 'space', '_schema')
+box.schema.user.grant('guest', 'read,write', 'space', '_space')
+box.schema.user.grant('guest', 'create', 'space')
 future1 = c:call('change_schema', {'1'}, {is_async = true})
 future2 = c:call('change_schema', {'2'}, {is_async = true})
 future3 = c:call('change_schema', {'3'}, {is_async = true})
@@ -1210,6 +1290,7 @@ s:drop()
 box.space.test1:drop()
 box.space.test2:drop()
 box.space.test3:drop()
+box.schema.func.drop('change_schema')
 
 --
 -- gh-3400: long-poll input discard must not touch event loop of
@@ -1238,4 +1319,6 @@ c._transport.perform_request(nil, nil, 'inject', nil, nil, data)
 c:close()
 test_run:grep_log('default', 'too big packet size in the header') ~= nil
 
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'write', 'space', '_schema')
+box.schema.user.revoke('guest', 'read,write', 'space', '_space')
+box.schema.user.revoke('guest', 'create', 'space')
diff --git a/test/box/net_msg_max.result b/test/box/net_msg_max.result
index ccda2014e..85872b65f 100644
--- a/test/box/net_msg_max.result
+++ b/test/box/net_msg_max.result
@@ -7,7 +7,13 @@ fiber = require('fiber')
 net_box = require('net.box')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.user.grant('guest', 'read', 'space', '_space')
+---
+...
+box.schema.func.create('do_long_f')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'do_long_f')
 ---
 ...
 conn = net_box.connect(box.cfg.listen)
@@ -252,7 +258,10 @@ conn2:close()
 conn:close()
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.func.drop('do_long_f')
+---
+...
+box.schema.user.revoke('guest', 'read', 'space', '_space')
 ---
 ...
 box.cfg{readahead = old_readahead, net_msg_max = limit}
diff --git a/test/box/net_msg_max.test.lua b/test/box/net_msg_max.test.lua
index 13f7050ed..c793b0f1c 100644
--- a/test/box/net_msg_max.test.lua
+++ b/test/box/net_msg_max.test.lua
@@ -3,7 +3,10 @@ test_run = require('test_run').new()
 fiber = require('fiber')
 net_box = require('net.box')
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.user.grant('guest', 'read', 'space', '_space')
+box.schema.func.create('do_long_f')
+box.schema.user.grant('guest', 'execute', 'function', 'do_long_f')
+
 conn = net_box.connect(box.cfg.listen)
 conn2 = net_box.connect(box.cfg.listen)
 active = 0
@@ -79,6 +82,7 @@ wait_finished(run_max)
 --
 -- Test minimal iproto msg count.
 --
+
 box.cfg{net_msg_max = 2}
 conn:ping()
 #conn.space._space:select{} > 0
@@ -136,5 +140,6 @@ wait_finished(110)
 conn2:close()
 conn:close()
 
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.func.drop('do_long_f')
+box.schema.user.revoke('guest', 'read', 'space', '_space')
 box.cfg{readahead = old_readahead, net_msg_max = limit}
diff --git a/test/box/on_replace.result b/test/box/on_replace.result
index fcdb43794..20ffe6412 100644
--- a/test/box/on_replace.result
+++ b/test/box/on_replace.result
@@ -540,7 +540,7 @@ s:select()
 ---
 - []
 ...
-s:drop()
+s:drop() -- test_on_repl_ddl
 ---
 ...
 --
diff --git a/test/box/on_replace.test.lua b/test/box/on_replace.test.lua
index 802aaaf2a..8a9fd3678 100644
--- a/test/box/on_replace.test.lua
+++ b/test/box/on_replace.test.lua
@@ -202,7 +202,7 @@ s:replace({8, 9})
 t = s:on_replace(function () s.index.pk:rename('newname') end, t)
 s:replace({9, 10})
 s:select()
-s:drop()
+s:drop() -- test_on_repl_ddl
 
 --
 -- gh-3020: sub-statement rollback
diff --git a/test/box/protocol.result b/test/box/protocol.result
index 3e43663b1..e03186cc9 100644
--- a/test/box/protocol.result
+++ b/test/box/protocol.result
@@ -1,6 +1,3 @@
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 --------------------------------------------------------------------------------
 -- Test case for #273: IPROTO_ITERATOR ignored in network protocol
 --------------------------------------------------------------------------------
@@ -10,6 +7,9 @@ space = box.schema.space.create('tweedledum')
 index = space:create_index('primary', { type = 'tree'})
 ---
 ...
+box.schema.user.grant('guest', 'read', 'space', 'tweedledum')
+---
+...
 for i=1,5 do space:insert{i} end
 ---
 ...
@@ -51,6 +51,3 @@ conn:close()
 space:drop()
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
diff --git a/test/box/protocol.test.lua b/test/box/protocol.test.lua
index 5026d585e..805a43721 100644
--- a/test/box/protocol.test.lua
+++ b/test/box/protocol.test.lua
@@ -1,4 +1,3 @@
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 
 --------------------------------------------------------------------------------
 -- Test case for #273: IPROTO_ITERATOR ignored in network protocol
@@ -6,6 +5,9 @@ box.schema.user.grant('guest', 'read,write,execute', 'universe')
 
 space = box.schema.space.create('tweedledum')
 index = space:create_index('primary', { type = 'tree'})
+
+box.schema.user.grant('guest', 'read', 'space', 'tweedledum')
+
 for i=1,5 do space:insert{i} end
 
 LISTEN = require('uri').parse(box.cfg.listen)
@@ -18,4 +20,3 @@ conn.space[space.id]:select(3, { iterator = 'LT' })
 conn:close()
 
 space:drop()
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
diff --git a/test/box/push.result b/test/box/push.result
index 4ec7c546c..af730c1a7 100644
--- a/test/box/push.result
+++ b/test/box/push.result
@@ -42,7 +42,10 @@ test_run:cmd("setopt delimiter ''");
 netbox = require('net.box')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.func.create('do_pushes')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'do_pushes')
 ---
 ...
 c = netbox.connect(box.cfg.listen)
@@ -78,6 +81,9 @@ finished = 0
 s = box.schema.create_space('test', {format = {{'field1', 'integer'}}})
 ---
 ...
+box.schema.user.grant('guest', 'write', 'space', 'test')
+---
+...
 pk = s:create_index('pk')
 ---
 ...
@@ -133,6 +139,12 @@ function dml_push_and_dml_f()
 end;
 ---
 ...
+box.schema.func.create('dml_push_and_dml');
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'dml_push_and_dml');
+---
+...
 -- At first check that a pushed message can be ignored in a binary
 -- protocol too.
 c:call('do_pushes', {300});
@@ -149,6 +161,8 @@ end;
 while finished ~= 400 do fiber.sleep(0.1) end;
 ---
 ...
+box.schema.func.drop('dml_push_and_dml')
+
 failed_catchers = {};
 ---
 ...
@@ -196,6 +210,12 @@ function push_null() box.session.push(box.NULL) end
 messages = {}
 ---
 ...
+box.schema.func.create('push_null')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'push_null')
+---
+...
 c:call('push_null', {}, {on_push = table.insert, on_push_ctx = messages})
 ---
 ...
@@ -203,6 +223,9 @@ messages
 ---
 - - null
 ...
+box.schema.func.drop('push_null')
+---
+...
 --
 -- Test binary pushes.
 --
@@ -272,6 +295,12 @@ t = setmetatable({100}, {__serialize = function() error('err in ser') end})
 function do_push() ok, err = box.session.push(t) end
 ---
 ...
+box.schema.func.create('do_push')
+---
+...
+box.schema.user.grant("guest", "execute", "function", "do_push")
+---
+...
 c:call('do_push', {}, {on_push = table.insert, on_push_ctx = messages})
 ---
 ...
@@ -284,6 +313,9 @@ messages
 ---
 - []
 ...
+box.schema.func.drop('do_push')
+---
+...
 --
 -- Test push from a non-call request.
 --
@@ -417,9 +449,18 @@ s:replace{1}
 ---
 - [1]
 ...
+box.schema.user.grant('guest', 'write', 'space', 'test')
+---
+...
 function do_push_and_duplicate() box.session.push(100) s:insert{1} end
 ---
 ...
+box.schema.func.create('do_push_and_duplicate')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'do_push_and_duplicate')
+---
+...
 future = c:call('do_push_and_duplicate', {}, {is_async = true})
 ---
 ...
@@ -447,12 +488,15 @@ keys
 - - 1
   - null
 ...
-s:drop()
+box.schema.func.drop('do_push_and_duplicate')
 ---
 ...
-c:close()
+box.schema.func.drop('do_pushes')
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+s:drop()
+---
+...
+c:close()
 ---
 ...
diff --git a/test/box/push.test.lua b/test/box/push.test.lua
index 36dc9eeb1..0d2bec3fe 100644
--- a/test/box/push.test.lua
+++ b/test/box/push.test.lua
@@ -23,7 +23,9 @@ end;
 test_run:cmd("setopt delimiter ''");
 
 netbox = require('net.box')
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+
+box.schema.func.create('do_pushes')
+box.schema.user.grant('guest', 'execute', 'function', 'do_pushes')
 
 c = netbox.connect(box.cfg.listen)
 c:ping()
@@ -37,6 +39,7 @@ catchers = {}
 started = 0
 finished = 0
 s = box.schema.create_space('test', {format = {{'field1', 'integer'}}})
+box.schema.user.grant('guest', 'write', 'space', 'test')
 pk = s:create_index('pk')
 c:reload_schema()
 test_run:cmd("setopt delimiter ';'")
@@ -77,6 +80,9 @@ function dml_push_and_dml_f()
     table.insert(catchers, catcher)
     finished = finished + 1
 end;
+box.schema.func.create('dml_push_and_dml');
+box.schema.user.grant('guest', 'execute', 'function', 'dml_push_and_dml');
+
 -- At first check that a pushed message can be ignored in a binary
 -- protocol too.
 c:call('do_pushes', {300});
@@ -87,6 +93,8 @@ for i = 1, 200 do
 end;
 while finished ~= 400 do fiber.sleep(0.1) end;
 
+box.schema.func.drop('dml_push_and_dml')
+
 failed_catchers = {};
 
 for _, c in pairs(catchers) do
@@ -121,9 +129,11 @@ failed_catchers
 --
 function push_null() box.session.push(box.NULL) end
 messages = {}
+box.schema.func.create('push_null')
+box.schema.user.grant('guest', 'execute', 'function', 'push_null')
 c:call('push_null', {}, {on_push = table.insert, on_push_ctx = messages})
 messages
-
+box.schema.func.drop('push_null')
 --
 -- Test binary pushes.
 --
@@ -148,10 +158,12 @@ err = nil
 messages = {}
 t = setmetatable({100}, {__serialize = function() error('err in ser') end})
 function do_push() ok, err = box.session.push(t) end
+box.schema.func.create('do_push')
+box.schema.user.grant("guest", "execute", "function", "do_push")
 c:call('do_push', {}, {on_push = table.insert, on_push_ctx = messages})
 ok, err
 messages
-
+box.schema.func.drop('do_push')
 --
 -- Test push from a non-call request.
 --
@@ -187,7 +199,6 @@ function do_pushes()
     return true
 end;
 test_run:cmd("setopt delimiter ''");
-
 -- Can not combine callback and async mode.
 ok, err = pcall(c.call, c, 'do_pushes', {}, {is_async = true, on_push = function() end})
 ok
@@ -209,7 +220,11 @@ s = box.schema.create_space('test')
 pk = s:create_index('pk')
 s:replace{1}
 
+box.schema.user.grant('guest', 'write', 'space', 'test')
+
 function do_push_and_duplicate() box.session.push(100) s:insert{1} end
+box.schema.func.create('do_push_and_duplicate')
+box.schema.user.grant('guest', 'execute', 'function', 'do_push_and_duplicate')
 future = c:call('do_push_and_duplicate', {}, {is_async = true})
 future:wait_result(1000)
 messages = {}
@@ -218,7 +233,7 @@ for i, message in future:pairs() do table.insert(messages, message) table.insert
 messages
 keys
 
+box.schema.func.drop('do_push_and_duplicate')
+box.schema.func.drop('do_pushes')
 s:drop()
 c:close()
-
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
diff --git a/test/box/schema_reload.result b/test/box/schema_reload.result
index 31f215bc3..c927e4fcf 100644
--- a/test/box/schema_reload.result
+++ b/test/box/schema_reload.result
@@ -1,6 +1,3 @@
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 net_box = require('net.box')
 ---
 ...
@@ -17,6 +14,9 @@ s = box.schema.create_space('test')
 i = s:create_index('primary')
 ---
 ...
+box.schema.user.grant('guest', 'read', 'space', 'test')
+---
+...
 cn = net_box.connect(LISTEN.host, LISTEN.service)
 ---
 ...
@@ -35,6 +35,9 @@ s2 = box.schema.create_space('test2')
 i2 = s2:create_index('primary')
 ---
 ...
+box.schema.user.grant('guest', 'read', 'space', 'test2')
+---
+...
 ----------------------------------
 -- TEST #1 simple reload
 ----------------------------------
@@ -123,6 +126,9 @@ s:drop()
 s2:drop()
 ---
 ...
+cn:close()
+---
+...
 --------------------------------------------------------------------------------
 -- gh-1808: support schema_version in CALL, EVAL and PING
 --------------------------------------------------------------------------------
@@ -177,6 +183,12 @@ bump_schema_version()
 function somefunc() return true end
 ---
 ...
+box.schema.func.create('somefunc')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'somefunc')
+---
+...
 cn:call('somefunc')
 ---
 - true
@@ -209,6 +221,18 @@ cn.schema_version == schema_version + 1
 ---
 - true
 ...
+box.schema.func.drop('somefunc')
+---
+...
+cn:close()
+---
+...
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
+cn = net_box.connect(box.cfg.listen)
+---
+...
 -- eval
 schema_version = cn.schema_version
 ---
@@ -271,6 +295,6 @@ box.internal.schema_version() == schema_version + 1
 if box.space.bump_schema_version ~= nil then box.space.bump_schema_version:drop() end
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'universe')
 ---
 ...
diff --git a/test/box/schema_reload.test.lua b/test/box/schema_reload.test.lua
index 48ccb169c..cdfbfca75 100644
--- a/test/box/schema_reload.test.lua
+++ b/test/box/schema_reload.test.lua
@@ -1,4 +1,3 @@
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 net_box = require('net.box')
 fiber = require('fiber')
 LISTEN = require('uri').parse(box.cfg.listen)
@@ -6,6 +5,7 @@ LISTEN = require('uri').parse(box.cfg.listen)
 -- create first space
 s = box.schema.create_space('test')
 i = s:create_index('primary')
+box.schema.user.grant('guest', 'read', 'space', 'test')
 cn = net_box.connect(LISTEN.host, LISTEN.service)
 
 -- check that schema is correct
@@ -15,6 +15,7 @@ old_schema_version = cn.schema_version
 -- create one more space
 s2 = box.schema.create_space('test2')
 i2 = s2:create_index('primary')
+box.schema.user.grant('guest', 'read', 'space', 'test2')
 
 ----------------------------------
 -- TEST #1 simple reload
@@ -66,6 +67,7 @@ request_fiber:cancel()
 reload_fiber:cancel()
 s:drop()
 s2:drop()
+cn:close()
 
 --------------------------------------------------------------------------------
 -- gh-1808: support schema_version in CALL, EVAL and PING
@@ -95,6 +97,8 @@ cn.schema_version == schema_version + 1
 schema_version = cn.schema_version
 bump_schema_version()
 function somefunc() return true end
+box.schema.func.create('somefunc')
+box.schema.user.grant('guest', 'execute', 'function', 'somefunc')
 cn:call('somefunc')
 wait_new_schema()
 cn.schema_version == schema_version + 1
@@ -107,6 +111,12 @@ cn:call('somefunc')
 wait_new_schema()
 cn.schema_version == schema_version + 1
 
+box.schema.func.drop('somefunc')
+
+cn:close()
+box.schema.user.grant('guest', 'execute', 'universe')
+cn = net_box.connect(box.cfg.listen)
+
 -- eval
 schema_version = cn.schema_version
 bump_schema_version()
@@ -125,6 +135,7 @@ somefunc = nil
 
 cn:close()
 
+
 -- box.internal.schema_version()
 schema_version = box.internal.schema_version()
 schema_version > 0
@@ -132,5 +143,4 @@ bump_schema_version()
 box.internal.schema_version() == schema_version + 1
 
 if box.space.bump_schema_version ~= nil then box.space.bump_schema_version:drop() end
-
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'universe')
diff --git a/test/box/sql.result b/test/box/sql.result
index 11a698850..a6c572f35 100644
--- a/test/box/sql.result
+++ b/test/box/sql.result
@@ -16,7 +16,10 @@ _ = box.schema.space.create('test1', { id = 555 })
 box.schema.user.create('test', { password = 'test' })
 ---
 ...
-box.schema.user.grant('test', 'execute,read,write', 'universe')
+box.schema.user.grant('test', 'read,write,alter', 'space', 'test1')
+---
+...
+box.schema.user.grant('test', 'read,write,alter', 'space', 'test')
 ---
 ...
 conn = net_box.connect('test:test@' .. box.cfg.listen)
@@ -228,10 +231,10 @@ net_box = require('net.box')
 box.schema.user.create('test', { password = 'test' })
 ---
 ...
-box.schema.user.grant('test', 'execute,read,write', 'universe')
+s = box.schema.space.create('tweedledum')
 ---
 ...
-s = box.schema.space.create('tweedledum')
+box.schema.user.grant('test', 'read,write,alter', 'space', 'tweedledum')
 ---
 ...
 index1 = s:create_index('primary', { type = 'tree', parts = { 1, 'string'} })
diff --git a/test/box/sql.test.lua b/test/box/sql.test.lua
index b1ba4168c..0523311ee 100644
--- a/test/box/sql.test.lua
+++ b/test/box/sql.test.lua
@@ -5,7 +5,8 @@ net_box = require('net.box')
 s = box.schema.space.create('test')
 _ = box.schema.space.create('test1', { id = 555 })
 box.schema.user.create('test', { password = 'test' })
-box.schema.user.grant('test', 'execute,read,write', 'universe')
+box.schema.user.grant('test', 'read,write,alter', 'space', 'test1')
+box.schema.user.grant('test', 'read,write,alter', 'space', 'test')
 
 conn = net_box.connect('test:test@' .. box.cfg.listen)
 space = conn.space.test
@@ -92,8 +93,8 @@ net_box = require('net.box')
 
 -- Prepare spaces
 box.schema.user.create('test', { password = 'test' })
-box.schema.user.grant('test', 'execute,read,write', 'universe')
 s = box.schema.space.create('tweedledum')
+box.schema.user.grant('test', 'read,write,alter', 'space', 'tweedledum')
 index1 = s:create_index('primary', { type = 'tree', parts = { 1, 'string'} })
 index2 = s:create_index('secondary', { type = 'tree', unique = false, parts = {2, 'string'}})
 function compare(a,b) return a[1] < b[1] end
diff --git a/test/box/stat_net.result b/test/box/stat_net.result
index fc1379920..b3e3db11f 100644
--- a/test/box/stat_net.result
+++ b/test/box/stat_net.result
@@ -19,7 +19,7 @@ box.stat.net.RECEIVED -- zero
 space = box.schema.space.create('tweedledum')
 ---
 ...
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest', 'read', 'space', 'tweedledum')
 ---
 ...
 index = space:create_index('primary', { type = 'hash' })
@@ -60,12 +60,9 @@ box.stat.net.RECEIVED.total
 ---
 - 0
 ...
-space:drop()
+space:drop() -- tweedledum
 ---
 ...
 cn:close()
 ---
 ...
-box.schema.user.revoke('guest','read,write,execute','universe')
----
-...
diff --git a/test/box/stat_net.test.lua b/test/box/stat_net.test.lua
index 9ddc55789..808bb71e7 100644
--- a/test/box/stat_net.test.lua
+++ b/test/box/stat_net.test.lua
@@ -7,7 +7,7 @@ box.stat.net.SENT -- zero
 box.stat.net.RECEIVED -- zero
 
 space = box.schema.space.create('tweedledum')
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest', 'read', 'space', 'tweedledum')
 index = space:create_index('primary', { type = 'hash' })
 remote = require 'net.box'
 
@@ -26,6 +26,5 @@ box.stat.reset()
 box.stat.net.SENT.total
 box.stat.net.RECEIVED.total
 
-space:drop()
+space:drop() -- tweedledum
 cn:close()
-box.schema.user.revoke('guest','read,write,execute','universe')
diff --git a/test/engine/params.result b/test/engine/params.result
index d5f4b3c82..debb40d40 100644
--- a/test/engine/params.result
+++ b/test/engine/params.result
@@ -8,9 +8,6 @@ inspector = test_run.new()
 engine = inspector:get_cfg('engine')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 s = box.schema.create_space('engine', {engine=engine})
 ---
 ...
@@ -30,6 +27,3 @@ box.space.engine:select{}
 box.space.engine:drop()
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
diff --git a/test/engine/params.test.lua b/test/engine/params.test.lua
index 3272835a1..a7cb499fd 100644
--- a/test/engine/params.test.lua
+++ b/test/engine/params.test.lua
@@ -3,7 +3,6 @@ test_run = require('test_run')
 inspector = test_run.new()
 engine = inspector:get_cfg('engine')
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 s = box.schema.create_space('engine', {engine=engine})
 i = s:create_index('primary')
 
@@ -14,4 +13,3 @@ box.space.engine:select{}
 
 -- cleanup
 box.space.engine:drop()
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
diff --git a/test/engine/replica_join.result b/test/engine/replica_join.result
index 39d857fef..7d6f50a19 100644
--- a/test/engine/replica_join.result
+++ b/test/engine/replica_join.result
@@ -10,9 +10,6 @@ engine = test_run:get_cfg('engine')
 index = test_run:get_cfg('index')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 box.schema.user.grant('guest', 'replication')
 ---
 ...
@@ -535,6 +532,3 @@ box.snapshot()
 box.schema.user.revoke('guest', 'replication')
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
diff --git a/test/engine/replica_join.test.lua b/test/engine/replica_join.test.lua
index 1792281e8..07320137b 100644
--- a/test/engine/replica_join.test.lua
+++ b/test/engine/replica_join.test.lua
@@ -2,7 +2,6 @@ env = require('test_run')
 test_run = env.new()
 engine = test_run:get_cfg('engine')
 index = test_run:get_cfg('index')
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 box.schema.user.grant('guest', 'replication')
 space = box.schema.space.create('test', { id = 99999, engine = engine })
 _ = space:create_index('primary', { type = index})
@@ -137,4 +136,3 @@ space:drop()
 box.snapshot()
 
 box.schema.user.revoke('guest', 'replication')
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
diff --git a/test/replication/autobootstrap.result b/test/replication/autobootstrap.result
index 04aeb4315..b660d0f2d 100644
--- a/test/replication/autobootstrap.result
+++ b/test/replication/autobootstrap.result
@@ -112,7 +112,16 @@ _ = test_run:cmd("switch autobootstrap1")
 u1 = box.schema.user.create('test_u')
 ---
 ...
-box.schema.user.grant('test_u', 'read,write,create', 'universe')
+box.schema.user.grant('test_u', 'create', 'space')
+---
+...
+box.schema.user.grant('test_u', 'read,write', 'space', '_space')
+---
+...
+box.schema.user.grant('test_u', 'write', 'space', '_schema')
+---
+...
+box.schema.user.grant('test_u', 'write', 'space', '_index')
 ---
 ...
 box.session.su('test_u')
@@ -131,6 +140,18 @@ box.space.test_u:select()
 ---
 - - [1, 2, 3, 4]
 ...
+box.schema.user.revoke('test_u', 'write', 'space', '_index')
+---
+...
+box.schema.user.revoke('test_u', 'write', 'space', '_schema')
+---
+...
+box.schema.user.revoke('test_u', 'read,write', 'space', '_space')
+---
+...
+box.schema.user.revoke('test_u', 'create', 'space')
+---
+...
 -- Synchronize
 vclock = test_run:get_vclock('autobootstrap1')
 ---
diff --git a/test/replication/autobootstrap.test.lua b/test/replication/autobootstrap.test.lua
index f1e2a9991..d952143b2 100644
--- a/test/replication/autobootstrap.test.lua
+++ b/test/replication/autobootstrap.test.lua
@@ -55,13 +55,21 @@ _ = test_run:cmd("switch default")
 
 _ = test_run:cmd("switch autobootstrap1")
 u1 = box.schema.user.create('test_u')
-box.schema.user.grant('test_u', 'read,write,create', 'universe')
+box.schema.user.grant('test_u', 'create', 'space')
+box.schema.user.grant('test_u', 'read,write', 'space', '_space')
+box.schema.user.grant('test_u', 'write', 'space', '_schema')
+box.schema.user.grant('test_u', 'write', 'space', '_index')
 box.session.su('test_u')
 _ = box.schema.space.create('test_u'):create_index('pk')
 box.session.su('admin')
 _ = box.space.test_u:replace({1, 2, 3, 4})
 box.space.test_u:select()
 
+box.schema.user.revoke('test_u', 'write', 'space', '_index')
+box.schema.user.revoke('test_u', 'write', 'space', '_schema')
+box.schema.user.revoke('test_u', 'read,write', 'space', '_space')
+box.schema.user.revoke('test_u', 'create', 'space')
+
 -- Synchronize
 vclock = test_run:get_vclock('autobootstrap1')
 _ = test_run:wait_vclock("autobootstrap2", vclock)
diff --git a/test/replication/catch.result b/test/replication/catch.result
index 91be32725..c8f8ca0a5 100644
--- a/test/replication/catch.result
+++ b/test/replication/catch.result
@@ -7,9 +7,6 @@ test_run = env.new()
 engine = test_run:get_cfg('engine')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 net_box = require('net.box')
 ---
 ...
@@ -139,6 +136,3 @@ box.space.test:drop()
 box.schema.user.revoke('guest', 'replication')
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
diff --git a/test/replication/catch.test.lua b/test/replication/catch.test.lua
index 2e2e97bc4..bba82e0e3 100644
--- a/test/replication/catch.test.lua
+++ b/test/replication/catch.test.lua
@@ -2,7 +2,6 @@ env = require('test_run')
 test_run = env.new()
 engine = test_run:get_cfg('engine')
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 
 net_box = require('net.box')
 errinj = box.error.injection
@@ -62,5 +61,4 @@ test_run:cmd("stop server replica")
 test_run:cmd("cleanup server replica")
 box.space.test:drop()
 box.schema.user.revoke('guest', 'replication')
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
 
diff --git a/test/replication/errinj.result b/test/replication/errinj.result
index ca8af2988..f2fa1df57 100644
--- a/test/replication/errinj.result
+++ b/test/replication/errinj.result
@@ -7,9 +7,6 @@ test_run = env.new()
 engine = test_run:get_cfg('engine')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 errinj = box.error.injection
 ---
 ...
diff --git a/test/replication/errinj.test.lua b/test/replication/errinj.test.lua
index 463d89a8f..eff9ef4b2 100644
--- a/test/replication/errinj.test.lua
+++ b/test/replication/errinj.test.lua
@@ -1,7 +1,6 @@
 env = require('test_run')
 test_run = env.new()
 engine = test_run:get_cfg('engine')
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 
 errinj = box.error.injection
 
diff --git a/test/replication/gc.result b/test/replication/gc.result
index e5c5cfccd..eeff6ac4b 100644
--- a/test/replication/gc.result
+++ b/test/replication/gc.result
@@ -27,9 +27,6 @@ function wait_gc(n) while #box.info.gc().checkpoints > n do fiber.sleep(0.01) en
 ---
 ...
 -- Grant permissions needed for replication.
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 box.schema.user.grant('guest', 'replication')
 ---
 ...
@@ -380,9 +377,6 @@ box.error.injection.set("ERRINJ_RELAY_REPORT_INTERVAL", 0)
 box.schema.user.revoke('guest', 'replication')
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
 box.cfg{checkpoint_count = default_checkpoint_count}
 ---
 ...
diff --git a/test/replication/gc.test.lua b/test/replication/gc.test.lua
index a465140c8..4034942cf 100644
--- a/test/replication/gc.test.lua
+++ b/test/replication/gc.test.lua
@@ -13,7 +13,6 @@ box.cfg{checkpoint_count = 1}
 function wait_gc(n) while #box.info.gc().checkpoints > n do fiber.sleep(0.01) end end
 
 -- Grant permissions needed for replication.
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 box.schema.user.grant('guest', 'replication')
 
 -- By default, relay thread reports status to tx once a second.
@@ -176,6 +175,5 @@ replica_set.drop_all(test_run)
 s:drop()
 box.error.injection.set("ERRINJ_RELAY_REPORT_INTERVAL", 0)
 box.schema.user.revoke('guest', 'replication')
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
 
 box.cfg{checkpoint_count = default_checkpoint_count}
diff --git a/test/replication/join_vclock.result b/test/replication/join_vclock.result
index 7c402dbf6..a9781073d 100644
--- a/test/replication/join_vclock.result
+++ b/test/replication/join_vclock.result
@@ -13,9 +13,6 @@ test_run = env.new()
 engine = test_run:get_cfg('engine')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 errinj = box.error.injection
 ---
 ...
@@ -87,9 +84,6 @@ replica_set.drop_all(test_run)
 box.space.test:drop()
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
 box.schema.user.revoke('guest', 'replication')
 ---
 ...
diff --git a/test/replication/join_vclock.test.lua b/test/replication/join_vclock.test.lua
index ac6eab75c..0b60dffc2 100644
--- a/test/replication/join_vclock.test.lua
+++ b/test/replication/join_vclock.test.lua
@@ -3,7 +3,6 @@ env = require('test_run')
 replica_set = require('fast_replica')
 test_run = env.new()
 engine = test_run:get_cfg('engine')
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 
 errinj = box.error.injection
 errinj.set("ERRINJ_RELAY_FINAL_SLEEP", true)
@@ -33,5 +32,4 @@ test_run:cmd("switch default")
 
 replica_set.drop_all(test_run)
 box.space.test:drop()
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
 box.schema.user.revoke('guest', 'replication')
diff --git a/test/replication/skip_conflict_row.result b/test/replication/skip_conflict_row.result
index bf794db5a..29963f56a 100644
--- a/test/replication/skip_conflict_row.result
+++ b/test/replication/skip_conflict_row.result
@@ -7,9 +7,6 @@ test_run = env.new()
 engine = test_run:get_cfg('engine')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 box.schema.user.grant('guest', 'replication')
 ---
 ...
@@ -100,6 +97,3 @@ box.space.test:drop()
 box.schema.user.revoke('guest', 'replication')
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
diff --git a/test/replication/skip_conflict_row.test.lua b/test/replication/skip_conflict_row.test.lua
index 695cce9db..5f7d6ead3 100644
--- a/test/replication/skip_conflict_row.test.lua
+++ b/test/replication/skip_conflict_row.test.lua
@@ -2,7 +2,6 @@ env = require('test_run')
 test_run = env.new()
 engine = test_run:get_cfg('engine')
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 box.schema.user.grant('guest', 'replication')
 
 space = box.schema.space.create('test', {engine = engine});
@@ -34,4 +33,3 @@ test_run:cmd("stop server replica")
 test_run:cmd("cleanup server replica")
 box.space.test:drop()
 box.schema.user.revoke('guest', 'replication')
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
diff --git a/test/vinyl/replica_quota.result b/test/vinyl/replica_quota.result
index 460cc1e61..50e397199 100644
--- a/test/vinyl/replica_quota.result
+++ b/test/vinyl/replica_quota.result
@@ -1,9 +1,6 @@
 test_run = require('test_run').new()
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 box.schema.user.grant('guest', 'replication')
 ---
 ...
@@ -94,6 +91,3 @@ s:drop()
 box.schema.user.revoke('guest', 'replication')
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
diff --git a/test/vinyl/replica_quota.test.lua b/test/vinyl/replica_quota.test.lua
index eade6f2f7..e04abbc22 100644
--- a/test/vinyl/replica_quota.test.lua
+++ b/test/vinyl/replica_quota.test.lua
@@ -1,6 +1,5 @@
 test_run = require('test_run').new()
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 box.schema.user.grant('guest', 'replication')
 
 s = box.schema.space.create('test', { engine = 'vinyl' })
@@ -49,4 +48,3 @@ _ = test_run:cmd("cleanup server replica")
 s:drop()
 
 box.schema.user.revoke('guest', 'replication')
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
diff --git a/test/wal_off/func_max.result b/test/wal_off/func_max.result
index 9211c4310..5a43821b2 100644
--- a/test/wal_off/func_max.result
+++ b/test/wal_off/func_max.result
@@ -36,36 +36,41 @@ function drop_limit_func()
 end;
 ---
 ...
-func_limit();
+test_run:cmd("setopt delimiter ''");
+---
+- true
+...
+func_limit()
 ---
 - error: 'A limit on the total number of functions has been reached: 32000'
 ...
-drop_limit_func();
+drop_limit_func()
 ---
 - error: Function 'func32000' does not exist
 ...
-box.schema.user.create('testuser');
+box.schema.user.create('testuser')
 ---
 ...
-box.schema.user.grant('testuser', 'read, write, execute,create', 'universe');
+box.schema.user.grant('testuser', 'read,write', 'space', '_func')
 ---
 ...
-session.su('testuser');
+box.schema.user.grant('testuser', 'create', 'function')
 ---
 ...
-func_limit();
+session.su('testuser')
+---
+...
+func_limit()
 ---
 - error: 'A limit on the total number of functions has been reached: 32000'
 ...
-drop_limit_func();
+drop_limit_func()
 ---
 - error: Function 'func32000' does not exist
 ...
 session.su('admin')
-box.schema.user.drop('testuser');
 ---
 ...
-test_run:cmd("setopt delimiter ''");
+box.schema.user.drop('testuser')
 ---
-- true
 ...
diff --git a/test/wal_off/func_max.test.lua b/test/wal_off/func_max.test.lua
index 00a095936..47fa6834b 100644
--- a/test/wal_off/func_max.test.lua
+++ b/test/wal_off/func_max.test.lua
@@ -21,13 +21,14 @@ function drop_limit_func()
         i = i + 1
     end
 end;
-func_limit();
-drop_limit_func();
-box.schema.user.create('testuser');
-box.schema.user.grant('testuser', 'read, write, execute,create', 'universe');
-session.su('testuser');
-func_limit();
-drop_limit_func();
-session.su('admin')
-box.schema.user.drop('testuser');
 test_run:cmd("setopt delimiter ''");
+func_limit()
+drop_limit_func()
+box.schema.user.create('testuser')
+box.schema.user.grant('testuser', 'read,write', 'space', '_func')
+box.schema.user.grant('testuser', 'create', 'function')
+session.su('testuser')
+func_limit()
+drop_limit_func()
+session.su('admin')
+box.schema.user.drop('testuser')
diff --git a/test/xlog/errinj.result b/test/xlog/errinj.result
index 262677f1d..6243ac701 100644
--- a/test/xlog/errinj.result
+++ b/test/xlog/errinj.result
@@ -50,15 +50,15 @@ test_run:cmd('restart server default with cleanup=1')
 errinj = box.error.injection
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 test = box.schema.create_space('test')
 ---
 ...
 _ = test:create_index('primary')
 ---
 ...
+box.schema.user.grant('guest', 'write', 'space', 'test')
+---
+...
 for i=1, box.cfg.rows_per_wal do test:insert{i, 'test'} end
 ---
 ...
@@ -85,6 +85,3 @@ test:drop()
 errinj = nil
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
diff --git a/test/xlog/errinj.test.lua b/test/xlog/errinj.test.lua
index 0ea15123f..7a5a29cb6 100644
--- a/test/xlog/errinj.test.lua
+++ b/test/xlog/errinj.test.lua
@@ -25,10 +25,11 @@ test_run:cmd('restart server default with cleanup=1')
 -- gh-881 iproto request with wal IO error
 errinj = box.error.injection
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 test = box.schema.create_space('test')
 _ = test:create_index('primary')
 
+box.schema.user.grant('guest', 'write', 'space', 'test')
+
 for i=1, box.cfg.rows_per_wal do test:insert{i, 'test'} end
 c = require('net.box').connect(box.cfg.listen)
 
@@ -40,4 +41,3 @@ errinj.set('ERRINJ_WAL_WRITE', false)
 -- Cleanup
 test:drop()
 errinj = nil
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
diff --git a/test/xlog/misc.result b/test/xlog/misc.result
index fd3362c4c..5df21aede 100644
--- a/test/xlog/misc.result
+++ b/test/xlog/misc.result
@@ -11,12 +11,12 @@ xlog = require('xlog')
 netbox = require('net.box')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 --
 -- Check that xlogs doesn't contain IPROTO_SYNC
 --
+box.schema.user.grant('guest', 'write', 'space', '_schema')
+---
+...
 conn = netbox.connect(box.cfg.listen)
 ---
 ...
@@ -68,9 +68,6 @@ box.space._schema:delete('test')
 --
 -- Clean up
 --
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
 netbox = nil
 ---
 ...
diff --git a/test/xlog/misc.test.lua b/test/xlog/misc.test.lua
index 6e67b86a9..44adfd942 100644
--- a/test/xlog/misc.test.lua
+++ b/test/xlog/misc.test.lua
@@ -5,12 +5,12 @@ fio = require('fio')
 xlog = require('xlog')
 netbox = require('net.box')
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
-
 --
 -- Check that xlogs doesn't contain IPROTO_SYNC
 --
 
+box.schema.user.grant('guest', 'write', 'space', '_schema')
+
 conn = netbox.connect(box.cfg.listen)
 -- insert some row using the binary protocol
 conn.space._schema:insert({'test'})
@@ -31,7 +31,6 @@ box.space._schema:delete('test')
 --
 -- Clean up
 --
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
 netbox = nil
 xlog = nil
 fio = nil
diff --git a/test/xlog/upgrade.result b/test/xlog/upgrade.result
index f02996bba..0bae10123 100644
--- a/test/xlog/upgrade.result
+++ b/test/xlog/upgrade.result
@@ -192,9 +192,9 @@ box.space._priv:select()
   - [1, 4, 'function', 3, 4]
   - [1, 4, 'role', 2, 4]
   - [1, 4, 'role', 5, 4]
-  - [1, 4, 'space', 513, 3]
+  - [1, 4, 'space', 513, 195]
   - [1, 4, 'universe', 0, 24]
-  - [1, 5, 'space', 512, 3]
+  - [1, 5, 'space', 512, 195]
   - [1, 31, 'universe', 0, 4294967295]
 ...
 box.space._vspace ~= nil
-- 
2.15.2 (Apple Git-101.1)

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [tarantool-patches] [PATCH v2 1/4] Make access_check_ddl check for entity privileges.
       [not found] ` <cover.1531843622.git.sergepetrenko@tarantool.org>
@ 2018-07-17 16:08   ` Serge Petrenko
  2018-07-17 16:08   ` [tarantool-patches] [PATCH v2 2/4] Add entities user, role to access control Serge Petrenko
                     ` (2 subsequent siblings)
  3 siblings, 0 replies; 11+ messages in thread
From: Serge Petrenko @ 2018-07-17 16:08 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Serge Petrenko

Function access_check_ddl checked only for universal access, thus
granting entity or singe object access to a user would have no effect in
scope of this function.
Fix this by adding entity access checks.

Also attaching an existing sequence to a space checked for
create privilege on both space (instead of alter) and sequence
(instead of read + write). Fixed both and changed the tests accordingly.

Now creating an index demands alter on a space, because checking for
create would lead to anyone with create access to entity space be able
to create indices in other users spaces. Also checking for alter or drop
on dropping an index, since any operations on space indices are altering
a space, and dropping a space requires dropping all indices, so have to
check for alter (in case of just dropping an index) or drop (in case of
dropping the space with all its indices, to not require additional alter
privilege).

Closes #3516
---
 src/box/alter.cc           |  53 +++++++++++++++----
 test/box/access.result     |  29 ++++++++--
 test/box/access.test.lua   |  21 ++++++--
 test/box/sequence.result   | 128 ++++++++++++++++++++++++++++++++++++++++-----
 test/box/sequence.test.lua |  58 +++++++++++++++-----
 5 files changed, 245 insertions(+), 44 deletions(-)

diff --git a/src/box/alter.cc b/src/box/alter.cc
index b74369321..bd50e457a 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -85,6 +85,13 @@ access_check_ddl(const char *name, uint32_t owner_uid,
 	user_access_t access = ((PRIV_U | (user_access_t) priv_type) &
 				~has_access);
 	bool is_owner = owner_uid == cr->uid || cr->uid == ADMIN;
+	if (access == 0)
+		return; /* Access granted. */
+	/* Check for specific entity access. */
+	struct access *object = entity_access_get(type);
+	if (object) {
+		access &= ~object[cr->auth_token].effective;
+	}
 	/*
 	 * Only the owner of the object or someone who has
 	 * specific DDL privilege on the object can execute
@@ -94,7 +101,7 @@ access_check_ddl(const char *name, uint32_t owner_uid,
 	 * the owner of the object, but this should be ignored --
 	 * CREATE privilege is required.
 	 */
-	if (access == 0 || (is_owner && !(access & (PRIV_U|PRIV_C))))
+	if (access == 0 || (is_owner && !(access & (PRIV_U | PRIV_C))))
 		return; /* Access granted. */
 
 	/* Create a meaningful error message. */
@@ -1764,11 +1771,20 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
 	uint32_t iid = tuple_field_u32_xc(old_tuple ? old_tuple : new_tuple,
 					  BOX_INDEX_FIELD_ID);
 	struct space *old_space = space_cache_find_xc(id);
-	enum priv_type priv_type = new_tuple ? PRIV_C : PRIV_D;
-	if (old_tuple && new_tuple)
-		priv_type = PRIV_A;
-	access_check_ddl(old_space->def->name, old_space->def->uid, SC_SPACE,
-			 priv_type, true);
+	bool have_alter = true;
+	try {
+		access_check_ddl(old_space->def->name, old_space->def->uid, SC_SPACE,
+				 PRIV_A, true);
+	} catch(AccessDeniedError *e) {
+		/*
+		 * We need alter access on space in case of creating or
+		 * altering an index. But we can have alter OR drop on
+		 * space in case of dropping an index.
+		 */
+		have_alter = false;
+		if(new_tuple)
+			throw;
+	}
 	struct index *old_index = space_index(old_space, iid);
 
 	/*
@@ -1826,6 +1842,13 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
 	 */
 	/* Case 1: drop the index, if it is dropped. */
 	if (old_index != NULL && new_tuple == NULL) {
+		/*
+		 * In case we don't have alter on space when
+		 * dropping an index, check for drop on space.
+		 */
+		if (!have_alter)
+		    access_check_ddl(old_space->def->name, old_space->def->uid,
+				     SC_SPACE, PRIV_D, true);
 		alter_space_move_indexes(alter, 0, iid);
 		(void) new DropIndex(alter, old_index->def);
 	}
@@ -3032,7 +3055,7 @@ on_replace_dd_sequence(struct trigger * /* trigger */, void *event)
 						      ER_CREATE_SEQUENCE);
 		assert(sequence_by_id(new_def->id) == NULL);
 		access_check_ddl(new_def->name, new_def->uid, SC_SEQUENCE,
-			PRIV_C, false);
+				 PRIV_C, false);
 		sequence_cache_replace(new_def);
 		alter->new_def = new_def;
 	} else if (old_tuple != NULL && new_tuple == NULL) {	/* DELETE */
@@ -3135,10 +3158,20 @@ on_replace_dd_space_sequence(struct trigger * /* trigger */, void *event)
 	enum priv_type priv_type = stmt->new_tuple ? PRIV_C : PRIV_D;
 	if (stmt->new_tuple && stmt->old_tuple)
 		priv_type = PRIV_A;
-
 	/* Check we have the correct access type on the sequence.  * */
-	access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE, priv_type,
-			 false);
+	if (is_generated || !stmt->new_tuple) {
+		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
+				 priv_type, false);
+	} else {
+		/*
+		 * In case user wants to attach an existing sequence,
+		 * check that it has read and write access.
+		 */
+		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
+				 PRIV_R, false);
+		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
+				 PRIV_W, false);
+	}
 	/** Check we have alter access on space. */
 	access_check_ddl(space->def->name, space->def->uid, SC_SPACE, PRIV_A,
 			 false);
diff --git a/test/box/access.result b/test/box/access.result
index a1f3e996a..f4669a4a3 100644
--- a/test/box/access.result
+++ b/test/box/access.result
@@ -875,7 +875,12 @@ session = box.session
 box.schema.user.create('test')
 ---
 ...
-box.schema.user.grant('test', 'read,write,create', 'universe')
+box.schema.user.grant('test', 'read', 'space', '_collation')
+---
+...
+--box.schema.user.grant('test', 'write', 'space', '_collation')
+-- FIXME: granting create on 'collation' only doesn't work
+box.schema.user.grant('test', 'create', 'universe')
 ---
 ...
 session.su('test')
@@ -1389,7 +1394,10 @@ box.schema.func.create('test_func')
 box.session.su("admin")
 ---
 ...
-box.schema.user.grant("tester", "read", "universe")
+box.schema.user.grant("tester", "read", "space", "_user")
+---
+...
+box.schema.user.grant("tester", "read", "space", "_func")
 ---
 ...
 -- failed create
@@ -1416,7 +1424,20 @@ box.session.su("admin")
 -- explicitly since we still use process_rw to write to system
 -- tables from ddl
 --
-box.schema.user.grant("tester", "create,write", "universe")
+box.schema.user.grant('tester', 'write', 'universe')
+---
+...
+-- no entity user currently, so have to grant create
+-- on universe in order to create a user.
+box.schema.user.grant('tester', 'create', 'universe')
+---
+...
+-- this should work instead:
+--box.schema.user.grant('tester', 'create', 'user')
+--box.schema.user.grant('tester', 'create', 'space')
+--box.schema.user.grant('tester', 'create', 'function')
+--box.schema.user.grant('tester', 'create' , 'sequence')
+box.schema.user.grant('tester', 'read', 'space', '_sequence')
 ---
 ...
 box.session.su("tester")
@@ -1824,7 +1845,7 @@ _  = box.schema.sequence.create('test_sequence')
 box.session.su('admin')
 ---
 ...
-box.schema.user.grant('tester', 'create', 'universe')
+box.schema.user.grant('tester', 'create', 'sequence')
 ---
 ...
 box.session.su('tester')
diff --git a/test/box/access.test.lua b/test/box/access.test.lua
index fb8f744e8..9ae0e1114 100644
--- a/test/box/access.test.lua
+++ b/test/box/access.test.lua
@@ -340,7 +340,10 @@ c:close()
 
 session = box.session
 box.schema.user.create('test')
-box.schema.user.grant('test', 'read,write,create', 'universe')
+box.schema.user.grant('test', 'read', 'space', '_collation')
+--box.schema.user.grant('test', 'write', 'space', '_collation')
+-- FIXME: granting create on 'collation' only doesn't work
+box.schema.user.grant('test', 'create', 'universe')
 session.su('test')
 box.internal.collation.create('test', 'ICU', 'ru_RU')
 session.su('admin')
@@ -520,7 +523,8 @@ box.schema.space.create("test_space")
 box.schema.user.create('test_user')
 box.schema.func.create('test_func')
 box.session.su("admin")
-box.schema.user.grant("tester", "read", "universe")
+box.schema.user.grant("tester", "read", "space", "_user")
+box.schema.user.grant("tester", "read", "space", "_func")
 -- failed create
 box.session.su("tester")
 box.schema.space.create("test_space")
@@ -533,7 +537,16 @@ box.session.su("admin")
 -- explicitly since we still use process_rw to write to system
 -- tables from ddl
 --
-box.schema.user.grant("tester", "create,write", "universe")
+box.schema.user.grant('tester', 'write', 'universe')
+-- no entity user currently, so have to grant create
+-- on universe in order to create a user.
+box.schema.user.grant('tester', 'create', 'universe')
+-- this should work instead:
+--box.schema.user.grant('tester', 'create', 'user')
+--box.schema.user.grant('tester', 'create', 'space')
+--box.schema.user.grant('tester', 'create', 'function')
+--box.schema.user.grant('tester', 'create' , 'sequence')
+box.schema.user.grant('tester', 'read', 'space', '_sequence')
 box.session.su("tester")
 -- successful create
 s1 = box.schema.space.create("test_space")
@@ -712,7 +725,7 @@ box.schema.user.grant('tester', 'read,write', 'space', '_sequence')
 box.session.su('tester')
 _  = box.schema.sequence.create('test_sequence')
 box.session.su('admin')
-box.schema.user.grant('tester', 'create', 'universe')
+box.schema.user.grant('tester', 'create', 'sequence')
 box.session.su('tester')
 _ = box.schema.sequence.create('test_sequence')
 box.session.su('admin')
diff --git a/test/box/sequence.result b/test/box/sequence.result
index cbbd45080..75d5ea1e6 100644
--- a/test/box/sequence.result
+++ b/test/box/sequence.result
@@ -1471,8 +1471,17 @@ box.session.su('admin')
 sq:drop()
 ---
 ...
+box.schema.user.revoke('user', 'read,write', 'universe')
+---
+...
 -- A user can alter/use sequences that he owns.
-box.schema.user.grant('user', 'create', 'universe')
+box.schema.user.grant('user', 'create', 'sequence')
+---
+...
+box.schema.user.grant('user', 'write', 'space', '_sequence')
+---
+...
+box.schema.user.grant('user', 'write', 'space', '_sequence_data')
 ---
 ...
 box.session.su('user')
@@ -1493,7 +1502,13 @@ sq = box.schema.sequence.create('seq')
 box.session.su('admin')
 ---
 ...
-box.schema.user.revoke('user', 'read,write,create', 'universe')
+box.schema.user.revoke('user', 'create', 'sequence')
+---
+...
+box.schema.user.revoke('user', 'write', 'space', '_sequence')
+---
+...
+box.schema.user.revoke('user', 'write', 'space', '_sequence_data')
 ---
 ...
 box.session.su('user')
@@ -1515,7 +1530,8 @@ box.session.su('admin')
 sq:drop()
 ---
 ...
--- A sequence can be attached to a space only if the user owns both.
+-- A sequence can be attached to a space only if the user has
+-- alter privilege on space and read/write on sequence.
 sq1 = box.schema.sequence.create('seq1')
 ---
 ...
@@ -1525,10 +1541,22 @@ s1 = box.schema.space.create('space1')
 _ = s1:create_index('pk')
 ---
 ...
-box.schema.user.grant('user', 'read,write', 'universe')
+box.schema.user.grant('user', 'write', 'space', '_sequence')
 ---
 ...
-box.schema.user.grant('user', 'create', 'universe')
+box.schema.user.grant('user', 'write', 'space', '_sequence_data')
+---
+...
+box.schema.user.grant('user', 'write', 'space', '_schema')
+---
+...
+box.schema.user.grant('user', 'write', 'space', '_space')
+---
+...
+box.schema.user.grant('user', 'create', 'space')
+---
+...
+box.schema.user.grant('user', 'create', 'sequence')
 ---
 ...
 box.session.su('user')
@@ -1540,17 +1568,53 @@ sq2 = box.schema.sequence.create('seq2')
 s2 = box.schema.space.create('space2')
 ---
 ...
--- fixme: no error on using another user's sequence
-_ = s2:create_index('pk', {sequence = 'seq1'})
+box.session.su('admin')
+---
+...
+box.schema.user.revoke('user', 'create', 'space')
+---
+...
+box.schema.user.revoke('user', 'create', 'sequence')
+---
+...
+box.schema.user.revoke('user', 'write', 'space', '_sequence')
+---
+...
+box.schema.user.revoke('user', 'write', 'space', '_sequence_data')
+---
+...
+box.schema.user.revoke('user', 'write', 'space', '_schema')
+---
+...
+box.schema.user.revoke('user', 'write', 'space', '_space')
+---
+...
+box.schema.user.grant('user', 'write', 'space', '_index')
+---
+...
+box.schema.user.grant('user', 'write', 'space', '_space_sequence')
 ---
 ...
+box.schema.user.grant('user', 'read', 'space', '_index')
+---
+...
+box.schema.user.grant('user', 'read', 'space', '_space_sequence')
+---
+...
+box.session.su('user')
+---
+...
+_ = s2:create_index('pk', {sequence = 'seq1'}) -- error
+---
+- error: Read access to sequence 'seq1' is denied for user 'user'
+...
 s1.index.pk:alter({sequence = 'seq1'}) -- error
 ---
 - error: Alter access to space 'space1' is denied for user 'user'
 ...
 box.space._space_sequence:replace{s1.id, sq1.id, false} -- error
 ---
-- error: Alter access to space 'space1' is denied for user 'user'
+- error: Read access to sequence 'seq1' is denied for user 'user'
 ...
 box.space._space_sequence:replace{s1.id, sq2.id, false} -- error
 ---
@@ -1558,7 +1622,7 @@ box.space._space_sequence:replace{s1.id, sq2.id, false} -- error
 ...
 box.space._space_sequence:replace{s2.id, sq1.id, false} -- error
 ---
-- error: Alter access to sequence 'seq1' is denied for user 'user'
+- error: Read access to sequence 'seq1' is denied for user 'user'
 ...
 s2.index.pk:alter({sequence = 'seq2'}) -- ok
 ---
@@ -1569,10 +1633,22 @@ box.session.su('admin')
 -- If the user owns a sequence attached to a space,
 -- it can use it for auto increment, otherwise it
 -- needs privileges.
-box.schema.user.revoke('user', 'read,write', 'universe')
+box.schema.user.revoke('user', 'write', 'space', '_index')
+---
+...
+box.schema.user.revoke('user', 'write', 'space', '_space_sequence')
+---
+...
+box.schema.user.revoke('user', 'read', 'space', '_space')
+---
+...
+box.schema.user.revoke('user', 'read', 'space', '_sequence')
+---
+...
+box.schema.user.revoke('user', 'read', 'space', '_index')
 ---
 ...
-box.schema.user.revoke('user', 'create', 'universe')
+box.schema.user.revoke('user', 'read', 'space', '_space_sequence')
 ---
 ...
 box.session.su('user')
@@ -1680,7 +1756,16 @@ s:drop()
 ---
 ...
 -- When a user is dropped, all his sequences are dropped as well.
-box.schema.user.grant('user', 'read,write,create', 'universe')
+box.schema.user.grant('user', 'write', 'space', '_sequence')
+---
+...
+box.schema.user.grant('user', 'read', 'space', '_sequence')
+---
+...
+box.schema.user.grant('user', 'write', 'space', '_space_sequence')
+---
+...
+box.schema.user.grant('user', 'create', 'sequence')
 ---
 ...
 box.session.su('user')
@@ -1710,10 +1795,25 @@ box.schema.user.create('user1')
 box.schema.user.create('user2')
 ---
 ...
-box.schema.user.grant('user1', 'read,write,create', 'universe')
+box.schema.user.grant('user1', 'create', 'sequence')
+---
+...
+box.schema.user.grant('user1', 'write', 'space', '_sequence')
+---
+...
+box.schema.user.grant('user1', 'read', 'space', '_sequence')
+---
+...
+box.schema.user.grant('user1', 'read', 'space', '_user')
+---
+...
+box.schema.user.grant('user1', 'write', 'space', '_sequence_data')
+---
+...
+box.schema.user.grant('user1', 'write', 'space', '_priv')
 ---
 ...
-box.schema.user.grant('user2', 'read,write,create', 'universe')
+box.schema.user.grant('user2', 'read,write', 'universe')
 ---
 ...
 box.session.su('user1')
diff --git a/test/box/sequence.test.lua b/test/box/sequence.test.lua
index c119459b3..00261e397 100644
--- a/test/box/sequence.test.lua
+++ b/test/box/sequence.test.lua
@@ -488,16 +488,21 @@ sq:alter{step = 2} -- error
 sq:drop() -- error
 box.session.su('admin')
 sq:drop()
+box.schema.user.revoke('user', 'read,write', 'universe')
 
 -- A user can alter/use sequences that he owns.
-box.schema.user.grant('user', 'create', 'universe')
+box.schema.user.grant('user', 'create', 'sequence')
+box.schema.user.grant('user', 'write', 'space', '_sequence')
+box.schema.user.grant('user', 'write', 'space', '_sequence_data')
 box.session.su('user')
 sq = box.schema.sequence.create('seq')
 sq:alter{step = 2} -- ok
 sq:drop() -- ok
 sq = box.schema.sequence.create('seq')
 box.session.su('admin')
-box.schema.user.revoke('user', 'read,write,create', 'universe')
+box.schema.user.revoke('user', 'create', 'sequence')
+box.schema.user.revoke('user', 'write', 'space', '_sequence')
+box.schema.user.revoke('user', 'write', 'space', '_sequence_data')
 box.session.su('user')
 sq:set(100) -- ok - user owns the sequence
 sq:next() -- ok
@@ -505,17 +510,34 @@ sq:reset() -- ok
 box.session.su('admin')
 sq:drop()
 
--- A sequence can be attached to a space only if the user owns both.
+-- A sequence can be attached to a space only if the user has
+-- alter privilege on space and read/write on sequence.
 sq1 = box.schema.sequence.create('seq1')
 s1 = box.schema.space.create('space1')
 _ = s1:create_index('pk')
-box.schema.user.grant('user', 'read,write', 'universe')
-box.schema.user.grant('user', 'create', 'universe')
+box.schema.user.grant('user', 'write', 'space', '_sequence')
+box.schema.user.grant('user', 'write', 'space', '_sequence_data')
+box.schema.user.grant('user', 'write', 'space', '_schema')
+box.schema.user.grant('user', 'write', 'space', '_space')
+box.schema.user.grant('user', 'create', 'space')
+box.schema.user.grant('user', 'create', 'sequence')
 box.session.su('user')
 sq2 = box.schema.sequence.create('seq2')
 s2 = box.schema.space.create('space2')
--- fixme: no error on using another user's sequence
-_ = s2:create_index('pk', {sequence = 'seq1'})
+
+box.session.su('admin')
+box.schema.user.revoke('user', 'create', 'space')
+box.schema.user.revoke('user', 'create', 'sequence')
+box.schema.user.revoke('user', 'write', 'space', '_sequence')
+box.schema.user.revoke('user', 'write', 'space', '_sequence_data')
+box.schema.user.revoke('user', 'write', 'space', '_schema')
+box.schema.user.revoke('user', 'write', 'space', '_space')
+box.schema.user.grant('user', 'write', 'space', '_index')
+box.schema.user.grant('user', 'write', 'space', '_space_sequence')
+box.schema.user.grant('user', 'read', 'space', '_index')
+box.schema.user.grant('user', 'read', 'space', '_space_sequence')
+box.session.su('user')
+_ = s2:create_index('pk', {sequence = 'seq1'}) -- error
 s1.index.pk:alter({sequence = 'seq1'}) -- error
 box.space._space_sequence:replace{s1.id, sq1.id, false} -- error
 box.space._space_sequence:replace{s1.id, sq2.id, false} -- error
@@ -526,8 +548,12 @@ box.session.su('admin')
 -- If the user owns a sequence attached to a space,
 -- it can use it for auto increment, otherwise it
 -- needs privileges.
-box.schema.user.revoke('user', 'read,write', 'universe')
-box.schema.user.revoke('user', 'create', 'universe')
+box.schema.user.revoke('user', 'write', 'space', '_index')
+box.schema.user.revoke('user', 'write', 'space', '_space_sequence')
+box.schema.user.revoke('user', 'read', 'space', '_space')
+box.schema.user.revoke('user', 'read', 'space', '_sequence')
+box.schema.user.revoke('user', 'read', 'space', '_index')
+box.schema.user.revoke('user', 'read', 'space', '_space_sequence')
 box.session.su('user')
 s2:insert{nil, 1} -- ok: {1, 1}
 box.session.su('admin')
@@ -563,7 +589,10 @@ box.session.su('admin')
 s:drop()
 
 -- When a user is dropped, all his sequences are dropped as well.
-box.schema.user.grant('user', 'read,write,create', 'universe')
+box.schema.user.grant('user', 'write', 'space', '_sequence')
+box.schema.user.grant('user', 'read', 'space', '_sequence')
+box.schema.user.grant('user', 'write', 'space', '_space_sequence')
+box.schema.user.grant('user', 'create', 'sequence')
 box.session.su('user')
 _ = box.schema.sequence.create('test1')
 _ = box.schema.sequence.create('test2')
@@ -575,8 +604,13 @@ box.sequence
 -- to a sequence.
 box.schema.user.create('user1')
 box.schema.user.create('user2')
-box.schema.user.grant('user1', 'read,write,create', 'universe')
-box.schema.user.grant('user2', 'read,write,create', 'universe')
+box.schema.user.grant('user1', 'create', 'sequence')
+box.schema.user.grant('user1', 'write', 'space', '_sequence')
+box.schema.user.grant('user1', 'read', 'space', '_sequence')
+box.schema.user.grant('user1', 'read', 'space', '_user')
+box.schema.user.grant('user1', 'write', 'space', '_sequence_data')
+box.schema.user.grant('user1', 'write', 'space', '_priv')
+box.schema.user.grant('user2', 'read,write', 'universe')
 box.session.su('user1')
 sq = box.schema.sequence.create('test')
 box.session.su('user2')
-- 
2.15.2 (Apple Git-101.1)

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [tarantool-patches] [PATCH v2 2/4] Add entities user, role to access control.
       [not found] ` <cover.1531843622.git.sergepetrenko@tarantool.org>
  2018-07-17 16:08   ` [tarantool-patches] [PATCH v2 1/4] Make access_check_ddl check for entity privileges Serge Petrenko
@ 2018-07-17 16:08   ` Serge Petrenko
  2018-07-17 16:08   ` [tarantool-patches] [PATCH v2 3/4] Add single object privilege checks to access_check_ddl Serge Petrenko
  2018-07-17 16:08   ` [tarantool-patches] [PATCH v2 4/4] Add a privilege upgrade script and update tests Serge Petrenko
  3 siblings, 0 replies; 11+ messages in thread
From: Serge Petrenko @ 2018-07-17 16:08 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Serge Petrenko

Previously the only existing entities in access control were space,
funciton and sequence. Added user and role entities, so it is now
possible to create users or roles without create privilege on universe.
Also added all the needed checks and modified tests accordingly.

Closes #3524
---
 src/box/alter.cc            | 72 +++++++++++++++++++++++++++++++++------------
 src/box/lua/schema.lua      | 27 ++++++++++-------
 src/box/schema.h            |  6 ++++
 src/box/user.cc             | 25 +++++++++++++++-
 test/box/access.result      | 20 ++++++-------
 test/box/access.test.lua    | 15 ++++------
 test/box/access_misc.result |  2 +-
 test/box/role.result        | 25 ++++++++++++++--
 test/box/role.test.lua      | 12 ++++++--
 9 files changed, 150 insertions(+), 54 deletions(-)

diff --git a/src/box/alter.cc b/src/box/alter.cc
index bd50e457a..6293dcc50 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -2186,7 +2186,7 @@ on_replace_dd_user(struct trigger * /* trigger */, void *event)
 	struct user *old_user = user_by_id(uid);
 	if (new_tuple != NULL && old_user == NULL) { /* INSERT */
 		struct user_def *user = user_def_new_from_tuple(new_tuple);
-		access_check_ddl(user->name, user->owner, SC_USER, PRIV_C, true);
+		access_check_ddl(user->name, user->owner, user->type, PRIV_C, true);
 		auto def_guard = make_scoped_guard([=] { free(user); });
 		(void) user_cache_replace(user);
 		def_guard.is_active = false;
@@ -2195,7 +2195,7 @@ on_replace_dd_user(struct trigger * /* trigger */, void *event)
 		txn_on_rollback(txn, on_rollback);
 	} else if (new_tuple == NULL) { /* DELETE */
 		access_check_ddl(old_user->def->name, old_user->def->owner,
-				 SC_USER, PRIV_D, true);
+				 old_user->def->type, PRIV_D, true);
 		/* Can't drop guest or super user */
 		if (uid <= (uint32_t) BOX_SYSTEM_USER_ID_MAX || uid == SUPER) {
 			tnt_raise(ClientError, ER_DROP_USER,
@@ -2221,6 +2221,14 @@ on_replace_dd_user(struct trigger * /* trigger */, void *event)
 		 * correct.
 		 */
 		struct user_def *user = user_def_new_from_tuple(new_tuple);
+		/* Do not allow changes for system users and roles. */
+		struct credentials *cr = effective_user();
+		if ((uid <= (uint32_t) BOX_SYSTEM_USER_ID_MAX || uid == SUPER) &&
+		    cr->uid != ADMIN) {
+			struct user *current_user = user_find_xc(cr->uid);
+			tnt_raise(AccessDeniedError, "alter", "user or role",
+				  old_user->def->name, current_user->def->name);
+		}
 		access_check_ddl(user->name, user->uid, SC_USER, PRIV_A,
 				 true);
 		auto def_guard = make_scoped_guard([=] { free(user); });
@@ -2641,26 +2649,49 @@ priv_def_check(struct priv_def *priv, enum priv_type priv_type)
 	}
 	case SC_ROLE:
 	{
-		struct user *role = user_by_id(priv->object_id);
-		if (role == NULL || role->def->type != SC_ROLE) {
-			tnt_raise(ClientError, ER_NO_SUCH_ROLE,
-				  role ? role->def->name :
-				  int2str(priv->object_id));
-		}
-		/*
-		 * Only the creator of the role can grant or revoke it.
-		 * Everyone can grant 'PUBLIC' role.
-		 */
-		if (role->def->owner != grantor->def->uid &&
-		    grantor->def->uid != ADMIN &&
-		    (role->def->uid != PUBLIC || priv->access != PRIV_X)) {
+		if (priv->object_id != 0) {
+			struct user *role = user_by_id(priv->object_id);
+			if (role == NULL || role->def->type != SC_ROLE) {
+				tnt_raise(ClientError, ER_NO_SUCH_ROLE,
+					  role ? role->def->name :
+					  int2str(priv->object_id));
+			}
+			/*
+			 * Only the creator of the role can grant or revoke it.
+			 * Everyone can grant 'PUBLIC' role.
+			 */
+			if (role->def->owner != grantor->def->uid &&
+			    grantor->def->uid != ADMIN &&
+			    (role->def->uid != PUBLIC || priv->access != PRIV_X)) {
+				tnt_raise(AccessDeniedError,
+					  priv_name(priv_type),
+					  schema_object_name(SC_ROLE), name,
+					  grantor->def->name);
+			}
+			/* Not necessary to do during revoke, but who cares. */
+			role_check(grantee, role);
+		} else if (grantor->def->uid != ADMIN) {
+			/* only admin may grant privileges on an entire entity. */
 			tnt_raise(AccessDeniedError,
 				  priv_name(priv_type),
 				  schema_object_name(SC_ROLE), name,
 				  grantor->def->name);
 		}
-		/* Not necessary to do during revoke, but who cares. */
-		role_check(grantee, role);
+		break;
+	}
+	case SC_USER:
+	{
+		struct user *user = NULL;
+		if (priv->object_id != 0)
+			user = user_by_id(priv->object_id);
+		if ((user == NULL || user->def->owner != grantor->def->uid) &&
+		    grantor->def->uid != ADMIN) {
+			tnt_raise(AccessDeniedError,
+				  priv_name(priv_type),
+				  schema_object_name(SC_USER), name,
+				  grantor->def->name);
+		}
+		break;
 	}
 	default:
 		break;
@@ -2681,7 +2712,12 @@ grant_or_revoke(struct priv_def *priv)
 	struct user *grantee = user_by_id(priv->grantee_id);
 	if (grantee == NULL)
 		return;
-	if (priv->object_type == SC_ROLE) {
+	/*
+	 * Grant a role to a user only when privilege type is 'execute'
+	 * and the role is specified.
+	 */
+	if (priv->object_type == SC_ROLE && !(priv->access & ~PRIV_X) &&
+	    priv->object_id != 0) {
 		struct user *role = user_by_id(priv->object_id);
 		if (role == NULL || role->def->type != SC_ROLE)
 			return;
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index ef544c879..4b7a14411 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -1739,6 +1739,8 @@ local priv_object_combo = {
                            box.priv.C, box.priv.D),
     ["role"]     = bit.bor(box.priv.X, box.priv.U,
                            box.priv.C, box.priv.D),
+    ["user"]	 = bit.bor(box.priv.C, box.priv.U,
+                           box.priv.D),
 }
 
 --
@@ -1842,21 +1844,25 @@ local function object_resolve(object_type, object_name)
         end
         return seq
     end
-    if object_type == 'role' then
+    if object_type == 'role' or object_type == 'user' then
         local _vuser = box.space[box.schema.VUSER_ID]
-        local role
+        local role_or_user
+        if object_name == nil or object_name == 0 then
+            return 0
+        end
         if type(object_name) == 'string' then
-            role = _vuser.index.name:get{object_name}
+            role_or_user = _vuser.index.name:get{object_name}
         else
-            role = _vuser:get{object_name}
+            role_or_user = _vuser:get{object_name}
         end
-        if role and role[4] == 'role' then
-            return role[1]
-        else
+        if role_or_user and role_or_user[4] == object_type then
+            return role_or_user[1]
+        elseif object_type == 'role' then
             box.error(box.error.NO_SUCH_ROLE, object_name)
-        end
+        else
+            box.error(box.error.NO_SUCH_USER, object_name)
+	end
     end
-
     box.error(box.error.UNKNOWN_SCHEMA_OBJECT, object_type)
 end
 
@@ -2102,7 +2108,8 @@ local function grant(uid, name, privilege, object_type,
     if privilege_hex ~= old_privilege then
         _priv:replace{options.grantor, uid, object_type, oid, privilege_hex}
     elseif not options.if_not_exists then
-            if object_type == 'role' then
+            if object_type == 'role' and object_name ~= nil and
+	       object_name ~= 0 then
                 box.error(box.error.ROLE_GRANTED, name, object_name)
             else
                 box.error(box.error.PRIV_GRANTED, name, privilege,
diff --git a/src/box/schema.h b/src/box/schema.h
index 0822262d0..f78ea43cc 100644
--- a/src/box/schema.h
+++ b/src/box/schema.h
@@ -240,6 +240,8 @@ struct on_access_denied_ctx {
 struct entity_access {
        struct access space[BOX_USER_MAX];
        struct access function[BOX_USER_MAX];
+       struct access user[BOX_USER_MAX];
+       struct access role[BOX_USER_MAX];
        struct access sequence[BOX_USER_MAX];
 };
 
@@ -255,6 +257,10 @@ entity_access_get(enum schema_object_type type)
                return entity_access.space;
        case SC_FUNCTION:
                return entity_access.function;
+       case SC_USER:
+	       return entity_access.user;
+       case SC_ROLE:
+	       return entity_access.role;
        case SC_SEQUENCE:
                return entity_access.sequence;
        default:
diff --git a/src/box/user.cc b/src/box/user.cc
index fbf06566a..4edef1d5f 100644
--- a/src/box/user.cc
+++ b/src/box/user.cc
@@ -229,6 +229,29 @@ access_find(struct priv_def *priv)
 			access = func->access;
 		break;
 	}
+	case SC_USER:
+	{
+		/*
+		 * user ID 0 is shared between user 'guest' and granting
+		 * privileges upon whole entity user. This is not a problem,
+		 * since we don't want to grant privileges on any system user,
+		 * including 'guest'.
+		 */
+		if(priv->object_id == 0) {
+			access = entity_access.user;
+			break;
+		}
+		/* No grants on a single object user yet. */
+	}
+	case SC_ROLE:
+	{
+		/* Tha same remark as in case SC_USER applies. */
+		if (priv->object_id == 0) {
+			access = entity_access.role;
+			break;
+		}
+		/* No grants on a single object role yet. */
+	}
 	case SC_SEQUENCE:
 	{
 		if (priv->object_id == 0) {
@@ -315,7 +338,7 @@ user_reload_privs(struct user *user)
 			 * Skip role grants, we're only
 			 * interested in real objects.
 			 */
-			if (priv.object_type != SC_ROLE)
+			if (priv.object_type != SC_ROLE || !(priv.access & PRIV_X))
 				user_grant_priv(user, &priv);
 		}
 	}
diff --git a/test/box/access.result b/test/box/access.result
index f4669a4a3..31095aec2 100644
--- a/test/box/access.result
+++ b/test/box/access.result
@@ -878,8 +878,6 @@ box.schema.user.create('test')
 box.schema.user.grant('test', 'read', 'space', '_collation')
 ---
 ...
---box.schema.user.grant('test', 'write', 'space', '_collation')
--- FIXME: granting create on 'collation' only doesn't work
 box.schema.user.grant('test', 'create', 'universe')
 ---
 ...
@@ -1427,16 +1425,18 @@ box.session.su("admin")
 box.schema.user.grant('tester', 'write', 'universe')
 ---
 ...
--- no entity user currently, so have to grant create
--- on universe in order to create a user.
-box.schema.user.grant('tester', 'create', 'universe')
+box.schema.user.grant('tester', 'create', 'user')
+---
+...
+box.schema.user.grant('tester', 'create', 'space')
+---
+...
+box.schema.user.grant('tester', 'create', 'function')
+---
+...
+box.schema.user.grant('tester', 'create' , 'sequence')
 ---
 ...
--- this should work instead:
---box.schema.user.grant('tester', 'create', 'user')
---box.schema.user.grant('tester', 'create', 'space')
---box.schema.user.grant('tester', 'create', 'function')
---box.schema.user.grant('tester', 'create' , 'sequence')
 box.schema.user.grant('tester', 'read', 'space', '_sequence')
 ---
 ...
diff --git a/test/box/access.test.lua b/test/box/access.test.lua
index 9ae0e1114..9b7510e64 100644
--- a/test/box/access.test.lua
+++ b/test/box/access.test.lua
@@ -341,8 +341,7 @@ c:close()
 session = box.session
 box.schema.user.create('test')
 box.schema.user.grant('test', 'read', 'space', '_collation')
---box.schema.user.grant('test', 'write', 'space', '_collation')
--- FIXME: granting create on 'collation' only doesn't work
+
 box.schema.user.grant('test', 'create', 'universe')
 session.su('test')
 box.internal.collation.create('test', 'ICU', 'ru_RU')
@@ -538,14 +537,10 @@ box.session.su("admin")
 -- tables from ddl
 --
 box.schema.user.grant('tester', 'write', 'universe')
--- no entity user currently, so have to grant create
--- on universe in order to create a user.
-box.schema.user.grant('tester', 'create', 'universe')
--- this should work instead:
---box.schema.user.grant('tester', 'create', 'user')
---box.schema.user.grant('tester', 'create', 'space')
---box.schema.user.grant('tester', 'create', 'function')
---box.schema.user.grant('tester', 'create' , 'sequence')
+box.schema.user.grant('tester', 'create', 'user')
+box.schema.user.grant('tester', 'create', 'space')
+box.schema.user.grant('tester', 'create', 'function')
+box.schema.user.grant('tester', 'create' , 'sequence')
 box.schema.user.grant('tester', 'read', 'space', '_sequence')
 box.session.su("tester")
 -- successful create
diff --git a/test/box/access_misc.result b/test/box/access_misc.result
index 839b576ac..0f4892533 100644
--- a/test/box/access_misc.result
+++ b/test/box/access_misc.result
@@ -361,7 +361,7 @@ testuser_uid = session.uid()
 ...
 _ = box.space._user:delete(2)
 ---
-- error: Drop access to user 'public' is denied for user 'testuser'
+- error: Drop access to role 'public' is denied for user 'testuser'
 ...
 box.space._user:select(1)
 ---
diff --git a/test/box/role.result b/test/box/role.result
index 806cea90b..243c7bc6c 100644
--- a/test/box/role.result
+++ b/test/box/role.result
@@ -214,7 +214,22 @@ box.schema.role.drop('test')
 box.schema.user.grant('grantee', 'liaison')
 ---
 ...
-box.schema.user.grant('test', 'read,write,create', 'universe')
+box.schema.user.grant('test', 'read,write', 'space', '_priv')
+---
+...
+box.schema.user.grant('test', 'write', 'space', '_schema')
+---
+...
+box.schema.user.grant('test', 'create', 'space')
+---
+...
+box.schema.user.grant('test', 'read,write', 'space', '_space')
+---
+...
+box.schema.user.grant('test', 'write', 'space', '_index')
+---
+...
+box.schema.user.grant('test', 'read', 'space', '_user')
 ---
 ...
 box.session.su('test')
@@ -635,7 +650,13 @@ box.schema.user.create('user')
 box.schema.user.create('grantee')
 ---
 ...
-box.schema.user.grant('user', 'read,write,execute,create', 'universe')
+box.schema.user.grant('user', 'read,write', 'space', '_user')
+---
+...
+box.schema.user.grant('user', 'read,write', 'space', '_priv')
+---
+...
+box.schema.user.grant('user', 'create', 'role')
 ---
 ...
 box.session.su('user')
diff --git a/test/box/role.test.lua b/test/box/role.test.lua
index e97339f49..9845f4c4c 100644
--- a/test/box/role.test.lua
+++ b/test/box/role.test.lua
@@ -69,7 +69,13 @@ box.schema.role.revoke('test', 'liaison')
 box.schema.role.drop('test')
 
 box.schema.user.grant('grantee', 'liaison')
-box.schema.user.grant('test', 'read,write,create', 'universe')
+
+box.schema.user.grant('test', 'read,write', 'space', '_priv')
+box.schema.user.grant('test', 'write', 'space', '_schema')
+box.schema.user.grant('test', 'create', 'space')
+box.schema.user.grant('test', 'read,write', 'space', '_space')
+box.schema.user.grant('test', 'write', 'space', '_index')
+box.schema.user.grant('test', 'read', 'space', '_user')
 box.session.su('test')
 s = box.schema.space.create('test')
 _ = s:create_index('i1')
@@ -248,7 +254,9 @@ box.schema.role.drop("role10")
 box.schema.user.create('user')
 box.schema.user.create('grantee')
 
-box.schema.user.grant('user', 'read,write,execute,create', 'universe')
+box.schema.user.grant('user', 'read,write', 'space', '_user')
+box.schema.user.grant('user', 'read,write', 'space', '_priv')
+box.schema.user.grant('user', 'create', 'role')
 box.session.su('user')
 box.schema.role.create('role')
 box.session.su('admin')
-- 
2.15.2 (Apple Git-101.1)

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [tarantool-patches] [PATCH v2 3/4] Add single object privilege checks to access_check_ddl.
       [not found] ` <cover.1531843622.git.sergepetrenko@tarantool.org>
  2018-07-17 16:08   ` [tarantool-patches] [PATCH v2 1/4] Make access_check_ddl check for entity privileges Serge Petrenko
  2018-07-17 16:08   ` [tarantool-patches] [PATCH v2 2/4] Add entities user, role to access control Serge Petrenko
@ 2018-07-17 16:08   ` Serge Petrenko
  2018-07-26 20:37     ` [tarantool-patches] " Konstantin Osipov
  2018-07-17 16:08   ` [tarantool-patches] [PATCH v2 4/4] Add a privilege upgrade script and update tests Serge Petrenko
  3 siblings, 1 reply; 11+ messages in thread
From: Serge Petrenko @ 2018-07-17 16:08 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Serge Petrenko

access_check_ddl() didn't check for single object privileges, e.g. user
with alter access on a space couldn't create an index in this space. It
would only succeed if it had alter on entire entity space.
Fix this by adding single object privilege checks to access_check_ddl and
adding access cache to struct user, to hold other users' privileges on it.

Also checking for single object privilege made it possible to grant
every user alter privilege on itself, so that a user may change its own
password (previously it was possible because of a hack). Removed the
hack, and added grant alter to itself upon user creation.
Modified tests accordingly, and added a couple of test cases.

Closes #3530
---
 src/box/alter.cc         | 123 ++++++++++++++++++++++----------
 src/box/lua/schema.lua   |   5 +-
 src/box/user.cc          |  10 ++-
 src/box/user.h           |   2 +
 test/box/access.result   | 182 +++++++++++++++++++++++++++++++++++++++++++++++
 test/box/access.test.lua |  53 ++++++++++++++
 test/box/role.result     |   9 +++
 test/box/sequence.result |   3 +
 8 files changed, 347 insertions(+), 40 deletions(-)

diff --git a/src/box/alter.cc b/src/box/alter.cc
index 6293dcc50..54a09664b 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -62,7 +62,8 @@
 /* {{{ Auxiliary functions and methods. */
 
 static void
-access_check_ddl(const char *name, uint32_t owner_uid,
+access_check_ddl(const char *name, uint32_t object_id,
+		 uint32_t owner_uid,
 		 enum schema_object_type type,
 		 enum priv_type priv_type,
 		 bool is_17_compat_mode)
@@ -103,7 +104,48 @@ access_check_ddl(const char *name, uint32_t owner_uid,
 	 */
 	if (access == 0 || (is_owner && !(access & (PRIV_U | PRIV_C))))
 		return; /* Access granted. */
-
+	/*
+	 * You can't grant CREATE privilege to a non-existing object.
+	 * USAGE can be granted only globally.
+	 */
+	if (!(access & (PRIV_U | PRIV_C))) {
+		/* Check for privileges on a single object. */
+		switch (type) {
+		case SC_SPACE:
+		{
+			struct space *space = space_by_id(object_id);
+			if (space)
+				access &= ~space->access[cr->auth_token].effective;
+			break;
+		}
+		case SC_FUNCTION:
+		{
+			struct func *func = func_by_id(object_id);
+			if (func)
+				access &= ~func->access[cr->auth_token].effective;
+			break;
+		}
+		case SC_USER:
+		case SC_ROLE:
+		{
+			struct user *user_or_role = user_by_id(object_id);
+			if (user_or_role)
+				access &= ~user_or_role->access[cr->auth_token].effective;
+			break;
+		}
+		case SC_SEQUENCE:
+		{
+			struct sequence *seq = sequence_by_id(object_id);
+			if (seq)
+				access &= ~seq->access[cr->auth_token].effective;
+			break;
+		}
+		default:
+			break;
+		}
+	}
+	if (access == 0)
+	    return; /* Access granted. */
 	/* Create a meaningful error message. */
 	struct user *user = user_find_xc(cr->uid);
 	const char *object_name;
@@ -1590,7 +1632,8 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 		struct space_def *def =
 			space_def_new_from_tuple(new_tuple, ER_CREATE_SPACE,
 						 region);
-		access_check_ddl(def->name, def->uid, SC_SPACE, PRIV_C, true);
+		access_check_ddl(def->name, def->id, def->uid, SC_SPACE,
+				 PRIV_C, true);
 		auto def_guard =
 			make_scoped_guard([=] { space_def_delete(def); });
 		RLIST_HEAD(empty_list);
@@ -1623,8 +1666,8 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 			txn_alter_trigger_new(on_create_space_rollback, space);
 		txn_on_rollback(txn, on_rollback);
 	} else if (new_tuple == NULL) { /* DELETE */
-		access_check_ddl(old_space->def->name, old_space->def->uid,
-				 SC_SPACE, PRIV_D, true);
+		access_check_ddl(old_space->def->name, old_space->def->id,
+				 old_space->def->uid, SC_SPACE, PRIV_D, true);
 		/* Verify that the space is empty (has no indexes) */
 		if (old_space->index_count) {
 			tnt_raise(ClientError, ER_DROP_SPACE,
@@ -1669,7 +1712,8 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 		struct space_def *def =
 			space_def_new_from_tuple(new_tuple, ER_ALTER_SPACE,
 						 region);
-		access_check_ddl(def->name, def->uid, SC_SPACE, PRIV_A, true);
+		access_check_ddl(def->name, def->id, def->uid, SC_SPACE,
+				 PRIV_A, true);
 		auto def_guard =
 			make_scoped_guard([=] { space_def_delete(def); });
 		if (def->id != space_id(old_space))
@@ -1773,8 +1817,8 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
 	struct space *old_space = space_cache_find_xc(id);
 	bool have_alter = true;
 	try {
-		access_check_ddl(old_space->def->name, old_space->def->uid, SC_SPACE,
-				 PRIV_A, true);
+		access_check_ddl(old_space->def->name, old_space->def->id,
+				 old_space->def->uid, SC_SPACE, PRIV_A, true);
 	} catch(AccessDeniedError *e) {
 		/*
 		 * We need alter access on space in case of creating or
@@ -1847,7 +1891,8 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
 		 * dropping an index, check for drop on space.
 		 */
 		if (!have_alter)
-		    access_check_ddl(old_space->def->name, old_space->def->uid,
+		    access_check_ddl(old_space->def->name, old_space->def->id,
+				     old_space->def->uid,
 				     SC_SPACE, PRIV_D, true);
 		alter_space_move_indexes(alter, 0, iid);
 		(void) new DropIndex(alter, old_index->def);
@@ -2186,7 +2231,8 @@ on_replace_dd_user(struct trigger * /* trigger */, void *event)
 	struct user *old_user = user_by_id(uid);
 	if (new_tuple != NULL && old_user == NULL) { /* INSERT */
 		struct user_def *user = user_def_new_from_tuple(new_tuple);
-		access_check_ddl(user->name, user->owner, user->type, PRIV_C, true);
+		access_check_ddl(user->name, user->uid, user->owner, user->type,
+				 PRIV_C, true);
 		auto def_guard = make_scoped_guard([=] { free(user); });
 		(void) user_cache_replace(user);
 		def_guard.is_active = false;
@@ -2194,7 +2240,8 @@ on_replace_dd_user(struct trigger * /* trigger */, void *event)
 			txn_alter_trigger_new(user_cache_remove_user, NULL);
 		txn_on_rollback(txn, on_rollback);
 	} else if (new_tuple == NULL) { /* DELETE */
-		access_check_ddl(old_user->def->name, old_user->def->owner,
+		access_check_ddl(old_user->def->name, old_user->def->uid,
+				 old_user->def->owner,
 				 old_user->def->type, PRIV_D, true);
 		/* Can't drop guest or super user */
 		if (uid <= (uint32_t) BOX_SYSTEM_USER_ID_MAX || uid == SUPER) {
@@ -2229,8 +2276,8 @@ on_replace_dd_user(struct trigger * /* trigger */, void *event)
 			tnt_raise(AccessDeniedError, "alter", "user or role",
 				  old_user->def->name, current_user->def->name);
 		}
-		access_check_ddl(user->name, user->uid, SC_USER, PRIV_A,
-				 true);
+		access_check_ddl(user->name, user->uid, user->owner, SC_USER,
+				 PRIV_A, true);
 		auto def_guard = make_scoped_guard([=] { free(user); });
 		struct trigger *on_commit =
 			txn_alter_trigger_new(user_cache_alter_user, NULL);
@@ -2332,7 +2379,8 @@ on_replace_dd_func(struct trigger * /* trigger */, void *event)
 	struct func *old_func = func_by_id(fid);
 	if (new_tuple != NULL && old_func == NULL) { /* INSERT */
 		struct func_def *def = func_def_new_from_tuple(new_tuple);
-		access_check_ddl(def->name, def->uid, SC_FUNCTION, PRIV_C, true);
+		access_check_ddl(def->name, def->fid, def->uid, SC_FUNCTION,
+				 PRIV_C, true);
 		auto def_guard = make_scoped_guard([=] { free(def); });
 		func_cache_replace(def);
 		def_guard.is_active = false;
@@ -2346,7 +2394,7 @@ on_replace_dd_func(struct trigger * /* trigger */, void *event)
 		 * Can only delete func if you're the one
 		 * who created it or a superuser.
 		 */
-		access_check_ddl(old_func->def->name, uid, SC_FUNCTION,
+		access_check_ddl(old_func->def->name, fid, uid, SC_FUNCTION,
 				 PRIV_D, true);
 		/* Can only delete func if it has no grants. */
 		if (schema_find_grants("function", old_func->def->fid)) {
@@ -2360,8 +2408,8 @@ on_replace_dd_func(struct trigger * /* trigger */, void *event)
 	} else {                                /* UPDATE, REPLACE */
 		struct func_def *def = func_def_new_from_tuple(new_tuple);
 		auto def_guard = make_scoped_guard([=] { free(def); });
-		access_check_ddl(def->name, def->uid, SC_FUNCTION, PRIV_A,
-				 true);
+		access_check_ddl(def->name, def->fid, def->uid,
+				 SC_FUNCTION, PRIV_A, true);
 		struct trigger *on_commit =
 			txn_alter_trigger_new(func_cache_replace_func, NULL);
 		txn_on_commit(txn, on_commit);
@@ -2517,8 +2565,9 @@ on_replace_dd_collation(struct trigger * /* trigger */, void *event)
 						    BOX_COLLATION_FIELD_ID);
 		struct coll_id *old_coll_id = coll_by_id(old_id);
 		assert(old_coll_id != NULL);
-		access_check_ddl(old_coll_id->name, old_coll_id->owner_id,
-				 SC_COLLATION, PRIV_D, false);
+		access_check_ddl(old_coll_id->name, old_id,
+				 old_coll_id->owner_id, SC_COLLATION, PRIV_D,
+				 false);
 		/*
 		 * Set on_commit/on_rollback triggers after
 		 * deletion from the cache to make trigger logic
@@ -2533,8 +2582,8 @@ on_replace_dd_collation(struct trigger * /* trigger */, void *event)
 		/* INSERT */
 		struct coll_id_def new_def;
 		coll_id_def_new_from_tuple(new_tuple, &new_def);
-		access_check_ddl(new_def.name, new_def.owner_id, SC_COLLATION,
-				 PRIV_C, false);
+		access_check_ddl(new_def.name, new_def.id, new_def.owner_id,
+				 SC_COLLATION, PRIV_C, false);
 		struct coll_id *new_coll_id = coll_id_new(&new_def);
 		if (new_coll_id == NULL)
 			diag_raise();
@@ -2593,8 +2642,8 @@ priv_def_check(struct priv_def *priv, enum priv_type priv_type)
 			  int2str(priv->grantee_id));
 	}
 	const char *name = schema_find_name(priv->object_type, priv->object_id);
-	access_check_ddl(name, grantor->def->uid, priv->object_type, priv_type,
-			 false);
+	access_check_ddl(name, priv->object_id, grantor->def->uid,
+			 priv->object_type, priv_type, false);
 	switch (priv->object_type) {
 	case SC_UNIVERSE:
 		if (grantor->def->uid != ADMIN) {
@@ -3090,8 +3139,8 @@ on_replace_dd_sequence(struct trigger * /* trigger */, void *event)
 		new_def = sequence_def_new_from_tuple(new_tuple,
 						      ER_CREATE_SEQUENCE);
 		assert(sequence_by_id(new_def->id) == NULL);
-		access_check_ddl(new_def->name, new_def->uid, SC_SEQUENCE,
-				 PRIV_C, false);
+		access_check_ddl(new_def->name, new_def->id, new_def->uid,
+				 SC_SEQUENCE, PRIV_C, false);
 		sequence_cache_replace(new_def);
 		alter->new_def = new_def;
 	} else if (old_tuple != NULL && new_tuple == NULL) {	/* DELETE */
@@ -3099,8 +3148,8 @@ on_replace_dd_sequence(struct trigger * /* trigger */, void *event)
 						 BOX_SEQUENCE_DATA_FIELD_ID);
 		struct sequence *seq = sequence_by_id(id);
 		assert(seq != NULL);
-		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
-				 PRIV_D, false);
+		access_check_ddl(seq->def->name, seq->def->id, seq->def->uid,
+				 SC_SEQUENCE, PRIV_D, false);
 		if (space_has_data(BOX_SEQUENCE_DATA_ID, 0, id))
 			tnt_raise(ClientError, ER_DROP_SEQUENCE,
 				  seq->def->name, "the sequence has data");
@@ -3116,8 +3165,8 @@ on_replace_dd_sequence(struct trigger * /* trigger */, void *event)
 						      ER_ALTER_SEQUENCE);
 		struct sequence *seq = sequence_by_id(new_def->id);
 		assert(seq != NULL);
-		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
-				 PRIV_A, false);
+		access_check_ddl(seq->def->name, seq->def->id, seq->def->uid,
+				 SC_SEQUENCE, PRIV_A, false);
 		alter->old_def = seq->def;
 		alter->new_def = new_def;
 	}
@@ -3196,21 +3245,21 @@ on_replace_dd_space_sequence(struct trigger * /* trigger */, void *event)
 		priv_type = PRIV_A;
 	/* Check we have the correct access type on the sequence.  * */
 	if (is_generated || !stmt->new_tuple) {
-		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
-				 priv_type, false);
+		access_check_ddl(seq->def->name, seq->def->id, seq->def->uid,
+				 SC_SEQUENCE, priv_type, false);
 	} else {
 		/*
 		 * In case user wants to attach an existing sequence,
 		 * check that it has read and write access.
 		 */
-		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
-				 PRIV_R, false);
-		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
-				 PRIV_W, false);
+		access_check_ddl(seq->def->name, seq->def->id, seq->def->uid,
+				 SC_SEQUENCE, PRIV_R, false);
+		access_check_ddl(seq->def->name, seq->def->id, seq->def->uid,
+				 SC_SEQUENCE, PRIV_W, false);
 	}
 	/** Check we have alter access on space. */
-	access_check_ddl(space->def->name, space->def->uid, SC_SPACE, PRIV_A,
-			 false);
+	access_check_ddl(space->def->name, space->def->id, space->def->uid,
+			 SC_SPACE, PRIV_A, false);
 
 	struct trigger *on_commit =
 		txn_alter_trigger_new(on_commit_dd_space_sequence, space);
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index 4b7a14411..a098e44fe 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -1740,7 +1740,7 @@ local priv_object_combo = {
     ["role"]     = bit.bor(box.priv.X, box.priv.U,
                            box.priv.C, box.priv.D),
     ["user"]	 = bit.bor(box.priv.C, box.priv.U,
-                           box.priv.D),
+                           box.priv.A, box.priv.D),
 }
 
 --
@@ -2056,6 +2056,9 @@ box.schema.user.create = function(name, opts)
     uid = _user:auto_increment{session.euid(), name, 'user', auth_mech_list}[1]
     -- grant role 'public' to the user
     box.schema.user.grant(uid, 'public')
+    -- grant user 'alter' on itself, so it can
+    -- change its password or username.
+    box.schema.user.grant(uid, 'alter', 'user', uid)
     -- we have to grant global privileges from setuid function, since
     -- only admin has the ownership over universe and we don't have
     -- grant option
diff --git a/src/box/user.cc b/src/box/user.cc
index 4edef1d5f..36e29f478 100644
--- a/src/box/user.cc
+++ b/src/box/user.cc
@@ -241,7 +241,10 @@ access_find(struct priv_def *priv)
 			access = entity_access.user;
 			break;
 		}
-		/* No grants on a single object user yet. */
+		struct user *user = user_by_id(priv->object_id);
+		if (user)
+			access = user->access;
+		break;
 	}
 	case SC_ROLE:
 	{
@@ -250,7 +253,10 @@ access_find(struct priv_def *priv)
 			access = entity_access.role;
 			break;
 		}
-		/* No grants on a single object role yet. */
+		struct user *role = user_by_id(priv->object_id);
+		if (role)
+			access = role->access;
+		break;
 	}
 	case SC_SEQUENCE:
 	{
diff --git a/src/box/user.h b/src/box/user.h
index 07c4dc504..069d9b77e 100644
--- a/src/box/user.h
+++ b/src/box/user.h
@@ -88,6 +88,8 @@ struct user
 	bool is_dirty;
 	/** Memory pool for privs */
 	struct region pool;
+	/** Cached runtime access imformation. */
+	struct access access[BOX_USER_MAX];
 };
 
 /** Find user by id. */
diff --git a/test/box/access.result b/test/box/access.result
index 31095aec2..9ea5568a5 100644
--- a/test/box/access.result
+++ b/test/box/access.result
@@ -136,6 +136,9 @@ box.schema.user.revoke('rich', 'read,write', 'universe')
 box.schema.user.revoke('rich', 'public')
 ---
 ...
+box.schema.user.revoke('rich', 'alter', 'user', 'rich')
+---
+...
 box.schema.user.disable("rich")
 ---
 ...
@@ -501,6 +504,7 @@ box.space._priv:select{id}
 ---
 - - [1, 32, 'role', 2, 4]
   - [1, 32, 'universe', 0, 27]
+  - [1, 32, 'user', 32, 128]
 ...
 box.schema.user.grant('user', 'read', 'universe')
 ---
@@ -510,6 +514,7 @@ box.space._priv:select{id}
 ---
 - - [1, 32, 'role', 2, 4]
   - [1, 32, 'universe', 0, 27]
+  - [1, 32, 'user', 32, 128]
 ...
 box.schema.user.revoke('user', 'write', 'universe')
 ---
@@ -518,6 +523,7 @@ box.space._priv:select{id}
 ---
 - - [1, 32, 'role', 2, 4]
   - [1, 32, 'universe', 0, 25]
+  - [1, 32, 'user', 32, 128]
 ...
 box.schema.user.revoke('user', 'read', 'universe')
 ---
@@ -526,6 +532,7 @@ box.space._priv:select{id}
 ---
 - - [1, 32, 'role', 2, 4]
   - [1, 32, 'universe', 0, 24]
+  - [1, 32, 'user', 32, 128]
 ...
 box.schema.user.grant('user', 'write', 'universe')
 ---
@@ -534,6 +541,7 @@ box.space._priv:select{id}
 ---
 - - [1, 32, 'role', 2, 4]
   - [1, 32, 'universe', 0, 26]
+  - [1, 32, 'user', 32, 128]
 ...
 box.schema.user.grant('user', 'read', 'universe')
 ---
@@ -542,6 +550,7 @@ box.space._priv:select{id}
 ---
 - - [1, 32, 'role', 2, 4]
   - [1, 32, 'universe', 0, 27]
+  - [1, 32, 'user', 32, 128]
 ...
 box.schema.user.drop('user')
 ---
@@ -965,6 +974,9 @@ box.schema.user.info('test_user')
   - - session,usage
     - universe
     - 
+  - - alter
+    - user
+    - test_user
 ...
 box.schema.role.info('test_role')
 ---
@@ -995,6 +1007,9 @@ box.schema.user.info('test_user')
   - - session,usage
     - universe
     - 
+  - - alter
+    - user
+    - test_user
 ...
 box.schema.role.info('test_role')
 ---
@@ -1860,3 +1875,170 @@ box.session.su('admin')
 box.schema.user.drop('tester')
 ---
 ...
+--
+-- test case for 3530: do not ignore single object privileges in
+-- access_check_ddl.
+--
+box.schema.user.create("test")
+---
+...
+_ = box.schema.space.create("space1")
+---
+...
+box.schema.user.grant("test", "read", "space", "space1")
+---
+...
+box.schema.user.grant("test", "write", "space", "_index")
+---
+...
+box.session.su("test")
+---
+...
+box.space.space1:create_index("pk")
+---
+- error: Alter access to space 'space1' is denied for user 'test'
+...
+box.session.su("admin")
+---
+...
+box.space.space1.index[0] == nil
+---
+- true
+...
+box.schema.user.grant("test", "alter", "space", "space1")
+---
+...
+box.session.su("test")
+---
+...
+_ = box.space.space1:create_index("pk")
+---
+...
+box.space.space1:insert{5}
+---
+- error: Write access to space 'space1' is denied for user 'test'
+...
+box.session.su("admin")
+---
+...
+box.space.space1.index[0] ~= nil
+---
+- true
+...
+box.space.space1:select{}
+---
+- []
+...
+box.schema.user.grant("test", "write", "space", "space1")
+---
+...
+box.session.su("test")
+---
+...
+box.space.space1:insert{5}
+---
+- [5]
+...
+box.session.su("admin")
+---
+...
+box.space.space1:select{}
+---
+- - [5]
+...
+box.schema.user.drop("test")
+---
+...
+box.space.space1:drop()
+---
+...
+--
+-- test that it is possible to grant privileges on a single user.
+box.schema.user.create("user1")
+---
+...
+box.schema.user.create("user2")
+---
+...
+box.schema.user.create("user3")
+---
+...
+box.schema.user.grant("user1", "write", "space", "_user")
+---
+...
+box.schema.user.grant("user1", "read", "space", "_user")
+---
+...
+box.space._user:select{}
+---
+- - [0, 1, 'guest', 'user', {'chap-sha1': 'vhvewKp0tNyweZQ+cFKAlsyphfg='}]
+  - [1, 1, 'admin', 'user', {}]
+  - [2, 1, 'public', 'role', {}]
+  - [3, 1, 'replication', 'role', {}]
+  - [31, 1, 'super', 'role', {}]
+  - [32, 1, 'user1', 'user', {}]
+  - [33, 1, 'user2', 'user', {}]
+  - [34, 1, 'user3', 'user', {}]
+...
+box.session.su("user1")
+---
+...
+-- can alter itself, but can't alter others without privileges.
+box.schema.user.passwd("user1", "abcd")
+---
+...
+box.schema.user.passwd("user2", "abcd")
+---
+- error: Alter access to user 'user2' is denied for user 'user1'
+...
+box.session.su("admin")
+---
+...
+box.space._user:select{}
+---
+- - [0, 1, 'guest', 'user', {'chap-sha1': 'vhvewKp0tNyweZQ+cFKAlsyphfg='}]
+  - [1, 1, 'admin', 'user', {}]
+  - [2, 1, 'public', 'role', {}]
+  - [3, 1, 'replication', 'role', {}]
+  - [31, 1, 'super', 'role', {}]
+  - [32, 1, 'user1', 'user', {'chap-sha1': 'oVTFJWXp5/lL/Aih/nAmJO2O/9o='}]
+  - [33, 1, 'user2', 'user', {}]
+  - [34, 1, 'user3', 'user', {}]
+...
+box.schema.user.grant("user1", "alter", "user", "user2")
+---
+...
+box.session.su("user1")
+---
+...
+box.schema.user.passwd("user2", "abcd")
+---
+...
+-- still fails
+box.schema.user.passwd("user3", "qewr")
+---
+- error: Alter access to user 'user3' is denied for user 'user1'
+...
+box.session.su("admin")
+---
+...
+box.space._user:select{}
+---
+- - [0, 1, 'guest', 'user', {'chap-sha1': 'vhvewKp0tNyweZQ+cFKAlsyphfg='}]
+  - [1, 1, 'admin', 'user', {}]
+  - [2, 1, 'public', 'role', {}]
+  - [3, 1, 'replication', 'role', {}]
+  - [31, 1, 'super', 'role', {}]
+  - [32, 1, 'user1', 'user', {'chap-sha1': 'oVTFJWXp5/lL/Aih/nAmJO2O/9o='}]
+  - [33, 1, 'user2', 'user', {'chap-sha1': 'oVTFJWXp5/lL/Aih/nAmJO2O/9o='}]
+  - [34, 1, 'user3', 'user', {}]
+...
+box.schema.user.drop("user1")
+---
+...
+box.schema.user.drop("user2")
+---
+...
+box.schema.user.drop("user3")
+---
+...
diff --git a/test/box/access.test.lua b/test/box/access.test.lua
index 9b7510e64..6ad7ee462 100644
--- a/test/box/access.test.lua
+++ b/test/box/access.test.lua
@@ -60,6 +60,7 @@ box.schema.func.drop('dummy')
 box.space['_user']:delete{uid}
 box.schema.user.revoke('rich', 'read,write', 'universe')
 box.schema.user.revoke('rich', 'public')
+box.schema.user.revoke('rich', 'alter', 'user', 'rich')
 box.schema.user.disable("rich")
 -- test double disable is a no op
 box.schema.user.disable("rich")
@@ -726,3 +727,55 @@ _ = box.schema.sequence.create('test_sequence')
 box.session.su('admin')
 box.schema.user.drop('tester')
 
+
+--
+-- test case for 3530: do not ignore single object privileges in
+-- access_check_ddl.
+--
+box.schema.user.create("test")
+_ = box.schema.space.create("space1")
+box.schema.user.grant("test", "read", "space", "space1")
+box.schema.user.grant("test", "write", "space", "_index")
+box.session.su("test")
+box.space.space1:create_index("pk")
+box.session.su("admin")
+box.space.space1.index[0] == nil
+box.schema.user.grant("test", "alter", "space", "space1")
+box.session.su("test")
+_ = box.space.space1:create_index("pk")
+box.space.space1:insert{5}
+box.session.su("admin")
+box.space.space1.index[0] ~= nil
+box.space.space1:select{}
+box.schema.user.grant("test", "write", "space", "space1")
+box.session.su("test")
+box.space.space1:insert{5}
+box.session.su("admin")
+box.space.space1:select{}
+box.schema.user.drop("test")
+box.space.space1:drop()
+
+--
+-- test that it is possible to grant privileges on a single user.
+box.schema.user.create("user1")
+box.schema.user.create("user2")
+box.schema.user.create("user3")
+box.schema.user.grant("user1", "write", "space", "_user")
+box.schema.user.grant("user1", "read", "space", "_user")
+box.space._user:select{}
+box.session.su("user1")
+-- can alter itself, but can't alter others without privileges.
+box.schema.user.passwd("user1", "abcd")
+box.schema.user.passwd("user2", "abcd")
+box.session.su("admin")
+box.space._user:select{}
+box.schema.user.grant("user1", "alter", "user", "user2")
+box.session.su("user1")
+box.schema.user.passwd("user2", "abcd")
+-- still fails
+box.schema.user.passwd("user3", "qewr")
+box.session.su("admin")
+box.space._user:select{}
+box.schema.user.drop("user1")
+box.schema.user.drop("user2")
+box.schema.user.drop("user3")
diff --git a/test/box/role.result b/test/box/role.result
index 243c7bc6c..5666f7ef7 100644
--- a/test/box/role.result
+++ b/test/box/role.result
@@ -49,6 +49,9 @@ box.schema.user.info('tester')
   - - session,usage
     - universe
     - 
+  - - alter
+    - user
+    - tester
 ...
 box.schema.user.grant('tester', 'execute', 'role', 'iddqd')
 ---
@@ -64,6 +67,9 @@ box.schema.user.info('tester')
   - - session,usage
     - universe
     - 
+  - - alter
+    - user
+    - tester
 ...
 -- test granting user to a user
 box.schema.user.grant('tester', 'execute', 'role', 'tester')
@@ -956,6 +962,9 @@ box.schema.user.info('test_user')
   - - session,usage
     - universe
     - 
+  - - alter
+    - user
+    - test_user
 ...
 box.schema.role.info('test_user')
 ---
diff --git a/test/box/sequence.result b/test/box/sequence.result
index 75d5ea1e6..a39a1155f 100644
--- a/test/box/sequence.result
+++ b/test/box/sequence.result
@@ -1362,6 +1362,9 @@ box.schema.user.info()
   - - session,usage
     - universe
     - 
+  - - alter
+    - user
+    - user
 ...
 sq:set(100) -- ok
 ---
-- 
2.15.2 (Apple Git-101.1)

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [tarantool-patches] [PATCH v2 4/4] Add a privilege upgrade script and update tests.
       [not found] ` <cover.1531843622.git.sergepetrenko@tarantool.org>
                     ` (2 preceding siblings ...)
  2018-07-17 16:08   ` [tarantool-patches] [PATCH v2 3/4] Add single object privilege checks to access_check_ddl Serge Petrenko
@ 2018-07-17 16:08   ` Serge Petrenko
  3 siblings, 0 replies; 11+ messages in thread
From: Serge Petrenko @ 2018-07-17 16:08 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Serge Petrenko

This patch adds a privilege upgrade script, which runs on upgrade to
1.10 and automatically grants CREATE,ALTER,DROP on objects and entities
to all users, who have READ and WRITE access on them.
Also all tests are rewritten to grant only necessary privileges, not
privileges to universe.

Closes #3539
---
 src/box/lua/upgrade.lua                     |  22 +++
 test/box-tap/auth.test.lua                  |   5 -
 test/box-tap/session.test.lua               |  15 +-
 test/box/access.result                      |   4 +-
 test/box/access.test.lua                    |   4 +-
 test/box/access_bin.result                  |   4 +-
 test/box/access_bin.test.lua                |   4 +-
 test/box/access_escalation.result           |  18 +-
 test/box/access_escalation.test.lua         |  11 +-
 test/box/access_misc.result                 |   4 +-
 test/box/access_misc.test.lua               |   4 +-
 test/box/call.result                        |   4 +-
 test/box/call.test.lua                      |   4 +-
 test/box/errinj.result                      |  30 +++-
 test/box/errinj.test.lua                    |  21 ++-
 test/box/net.box.result                     | 265 ++++++++++++++++++++++++++--
 test/box/net.box.test.lua                   | 121 +++++++++++--
 test/box/net_msg_max.result                 |  13 +-
 test/box/net_msg_max.test.lua               |   9 +-
 test/box/on_replace.result                  |   2 +-
 test/box/on_replace.test.lua                |   2 +-
 test/box/protocol.result                    |   9 +-
 test/box/protocol.test.lua                  |   5 +-
 test/box/push.result                        |  52 +++++-
 test/box/push.test.lua                      |  27 ++-
 test/box/schema_reload.result               |  32 +++-
 test/box/schema_reload.test.lua             |  16 +-
 test/box/sql.result                         |   9 +-
 test/box/sql.test.lua                       |   5 +-
 test/box/stat_net.result                    |   7 +-
 test/box/stat_net.test.lua                  |   5 +-
 test/engine/params.result                   |   6 -
 test/engine/params.test.lua                 |   2 -
 test/engine/replica_join.result             |   6 -
 test/engine/replica_join.test.lua           |   2 -
 test/replication/autobootstrap.result       |  23 ++-
 test/replication/autobootstrap.test.lua     |  10 +-
 test/replication/catch.result               |   6 -
 test/replication/catch.test.lua             |   2 -
 test/replication/errinj.result              |   3 -
 test/replication/errinj.test.lua            |   1 -
 test/replication/gc.result                  |   6 -
 test/replication/gc.test.lua                |   2 -
 test/replication/join_vclock.result         |   6 -
 test/replication/join_vclock.test.lua       |   2 -
 test/replication/skip_conflict_row.result   |   6 -
 test/replication/skip_conflict_row.test.lua |   2 -
 test/vinyl/replica_quota.result             |   6 -
 test/vinyl/replica_quota.test.lua           |   2 -
 test/wal_off/func_max.result                |  25 +--
 test/wal_off/func_max.test.lua              |  19 +-
 test/xlog/errinj.result                     |   9 +-
 test/xlog/errinj.test.lua                   |   4 +-
 test/xlog/misc.result                       |   9 +-
 test/xlog/misc.test.lua                     |   5 +-
 test/xlog/upgrade.result                    |   4 +-
 56 files changed, 676 insertions(+), 225 deletions(-)

diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua
index 0293f6ef8..e738d48e4 100644
--- a/src/box/lua/upgrade.lua
+++ b/src/box/lua/upgrade.lua
@@ -960,8 +960,30 @@ local function create_vsequence_space()
     box.space._vsequence:format(sequence_format)
 end
 
+local function upgrade_users_to_1_10_0()
+    local _priv = box.space[box.schema.PRIV_ID]
+    local _user = box.space[box.schema.USER_ID]
+
+    for _, user in _user:pairs() do
+        if user[0] ~= ADMIN and user[0] ~= SUPER then
+            for _, priv in _priv:pairs(user[0]) do
+                if bit.band(priv[5], box.priv.W) ~= 0 and
+                bit.band(priv[5], box.priv.R) ~= 0 then
+                    local new_privs = bit.bor(box.priv.A, box.priv.D)
+                    if priv[4] == 0 then
+                        new_privs = bit.bor(new_privs, box.priv.C)
+                    end
+                    _priv:update({priv[2], priv[3], priv[4]},
+                                 {{ "|", 5, new_privs}})
+                end
+            end
+        end
+    end
+end
+
 local function upgrade_to_1_10_0()
     create_vsequence_space()
+    upgrade_users_to_1_10_0()
 end
 
 
diff --git a/test/box-tap/auth.test.lua b/test/box-tap/auth.test.lua
index 272bd97dc..4e9879408 100755
--- a/test/box-tap/auth.test.lua
+++ b/test/box-tap/auth.test.lua
@@ -20,10 +20,7 @@ test:plan(42)
 local space = box.schema.space.create('tweedledum')
 local index = space:create_index('primary', { type = 'hash' })
 box.schema.user.create('test', {password='pass'})
-box.schema.user.grant('test', 'read,write,execute', 'universe')
 box.schema.user.create('test2', {password=''})
-box.schema.user.grant('test2', 'read,write,execute', 'universe')
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 
 -- check how authentication trigger work
 local msg, counter, succeeded
@@ -163,8 +160,6 @@ test:is(session.sync(), 0, "box.session.sync()")
 
 -- cleanup
 space:drop()
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
-box.schema.user.revoke('test', 'read,write,execute', 'universe')
 box.schema.user.drop('test', { if_exists = true})
 box.schema.user.drop("test2", { if_exists = true})
 
diff --git a/test/box-tap/session.test.lua b/test/box-tap/session.test.lua
index c3c07a67c..857bc643b 100755
--- a/test/box-tap/session.test.lua
+++ b/test/box-tap/session.test.lua
@@ -101,7 +101,8 @@ function audit_disconnect() box.space['tweedledum']:delete{session.id()} end
 test:is(type(session.on_connect(audit_connect)), "function", "type of trigger audit_connect on_connect")
 test:is(type(session.on_disconnect(audit_disconnect)), "function", "type of trigger audit_connect on_disconnect")
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.user.grant('guest', 'read,write', 'space', 'tweedledum')
+box.schema.user.grant('guest', 'execute', 'universe')
 a = net.box.connect(HOST, PORT)
 test:ok(a:eval('return space:get{box.session.id()}[1] == session.id()'), "eval get_id")
 test:ok(a:eval('return session.sync() ~= 0'), "eval sync")
@@ -112,12 +113,12 @@ session.on_connect(nil, audit_connect)
 session.on_disconnect(nil, audit_disconnect)
 test:is(active_connections, 0, "active connections after other triggers")
 
-space:drop()
+space:drop() -- tweedledum
 
 test:is(session.uid(), 1, "uid == 1")
 test:is(session.user(), "admin", "user is admin")
 test:is(session.sync(), 0, "sync constant")
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'universe')
 
 -- audit permission in on_connect/on_disconnect triggers
 box.schema.user.create('tester', { password = 'tester' })
@@ -199,7 +200,10 @@ function f2()
 	sync2 = box.session.sync()
 	cond:signal()
 end
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.func.create('f1')
+box.schema.func.create('f2')
+box.schema.user.grant('guest', 'execute', 'function', 'f1')
+box.schema.user.grant('guest', 'execute', 'function', 'f2')
 conn = net.box.connect(box.cfg.listen)
 test:ok(conn:ping(), 'connect to self')
 _ = fiber.create(function() conn:call('f1') end)
@@ -208,7 +212,8 @@ _ = fiber.create(function() conn:call('f2') end)
 while started ~= 2 do fiber.sleep(0.01) end
 test:isnt(sync1, sync2, 'session.sync() is request local')
 conn:close()
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'function', 'f1')
+box.schema.user.revoke('guest', 'execute', 'function', 'f2')
 
 inspector:cmd('stop server session with cleanup=1')
 session = nil
diff --git a/test/box/access.result b/test/box/access.result
index 9ea5568a5..f39678e26 100644
--- a/test/box/access.result
+++ b/test/box/access.result
@@ -370,7 +370,7 @@ box.schema.user.drop('uniuser')
 box.schema.user.create('grantor')
 ---
 ...
-box.schema.user.grant('grantor', 'read, write, execute', 'universe')
+box.schema.user.grant('grantor', 'read, write, execute, create, alter, drop', 'universe')
 ---
 ...
 session.su('grantor')
@@ -379,7 +379,7 @@ session.su('grantor')
 box.schema.user.create('grantee')
 ---
 ...
-box.schema.user.grant('grantee', 'read, write, execute', 'universe')
+box.schema.user.grant('grantee', 'read, write, execute, create, alter, drop', 'universe')
 ---
 - error: Grant access to universe '' is denied for user 'grantor'
 ...
diff --git a/test/box/access.test.lua b/test/box/access.test.lua
index 6ad7ee462..ee62df5fb 100644
--- a/test/box/access.test.lua
+++ b/test/box/access.test.lua
@@ -167,10 +167,10 @@ box.schema.user.drop('uniuser')
 -- only by its creator at the moment
 -- ------------------------------------------------------------
 box.schema.user.create('grantor')
-box.schema.user.grant('grantor', 'read, write, execute', 'universe')
+box.schema.user.grant('grantor', 'read, write, execute, create, alter, drop', 'universe')
 session.su('grantor')
 box.schema.user.create('grantee')
-box.schema.user.grant('grantee', 'read, write, execute', 'universe')
+box.schema.user.grant('grantee', 'read, write, execute, create, alter, drop', 'universe')
 session.su('grantee')
 -- fails - can't suicide - ask the creator to kill you
 box.schema.user.drop('grantee')
diff --git a/test/box/access_bin.result b/test/box/access_bin.result
index 7b30d11f2..df8ef8dee 100644
--- a/test/box/access_bin.result
+++ b/test/box/access_bin.result
@@ -8,7 +8,7 @@ test_run = env.new()
 -- Access control tests which require a binary protocol
 -- connection to the server
 --
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest','execute','universe')
 ---
 ...
 session = box.session
@@ -30,7 +30,7 @@ c:eval("return session.user()")
 c:close()
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'universe')
 ---
 ...
 -- gh-488 suid functions
diff --git a/test/box/access_bin.test.lua b/test/box/access_bin.test.lua
index 4c7a6d08f..e77d8c0a8 100644
--- a/test/box/access_bin.test.lua
+++ b/test/box/access_bin.test.lua
@@ -4,14 +4,14 @@ test_run = env.new()
 -- Access control tests which require a binary protocol
 -- connection to the server
 --
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest','execute','universe')
 session = box.session
 remote = require('net.box')
 c = remote.connect(box.cfg.listen)
 c:eval("session.su('admin')")
 c:eval("return session.user()")
 c:close()
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'universe')
 
 -- gh-488 suid functions
 --
diff --git a/test/box/access_escalation.result b/test/box/access_escalation.result
index a83f1ee8a..1992ba46d 100644
--- a/test/box/access_escalation.result
+++ b/test/box/access_escalation.result
@@ -84,7 +84,7 @@ box.schema.user.create('underprivileged')
 box.schema.user.grant('underprivileged', 'read,write', 'space', '_func')
 ---
 ...
-box.schema.user.grant('underprivileged', 'create', 'universe')
+box.schema.user.grant('underprivileged', 'create', 'function')
 ---
 ...
 box.session.su('underprivileged')
@@ -99,7 +99,16 @@ box.session.su('admin')
 --
 -- create a deprived function
 --
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.func.create('escalation')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'setuid')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'escalation')
+---
+...
+box.schema.user.grant('guest', 'read', 'space', '_space')
 ---
 ...
 connection = net:connect(os.getenv("LISTEN"))
@@ -119,7 +128,10 @@ fiber.cancel(background)
 box.schema.user.drop('underprivileged')
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.func.drop('escalation')
+---
+...
+box.schema.user.revoke('guest', 'read', 'space', '_space')
 ---
 ...
 connection:close()
diff --git a/test/box/access_escalation.test.lua b/test/box/access_escalation.test.lua
index 29b14c8ea..b333eccf4 100644
--- a/test/box/access_escalation.test.lua
+++ b/test/box/access_escalation.test.lua
@@ -61,7 +61,7 @@ connection:close()
 
 box.schema.user.create('underprivileged')
 box.schema.user.grant('underprivileged', 'read,write', 'space', '_func')
-box.schema.user.grant('underprivileged', 'create', 'universe')
+box.schema.user.grant('underprivileged', 'create', 'function')
 box.session.su('underprivileged')
 box.schema.func.create('setuid', {setuid=true})
 box.session.su('admin')
@@ -69,7 +69,10 @@ box.session.su('admin')
 -- create a deprived function
 --
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.func.create('escalation')
+box.schema.user.grant('guest', 'execute', 'function', 'setuid')
+box.schema.user.grant('guest', 'execute', 'function', 'escalation')
+box.schema.user.grant('guest', 'read', 'space', '_space')
 
 connection = net:connect(os.getenv("LISTEN"))
 
@@ -80,6 +83,6 @@ fiber.cancel(background)
 -- tear down
 
 box.schema.user.drop('underprivileged')
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
-
+box.schema.func.drop('escalation')
+box.schema.user.revoke('guest', 'read', 'space', '_space')
 connection:close()
diff --git a/test/box/access_misc.result b/test/box/access_misc.result
index 0f4892533..9291c413a 100644
--- a/test/box/access_misc.result
+++ b/test/box/access_misc.result
@@ -208,7 +208,7 @@ s:select()
 box.schema.user.create('uniuser')
 ---
 ...
-box.schema.user.grant('uniuser', 'read, write, execute', 'universe')
+box.schema.user.grant('uniuser', 'read, write, execute, create', 'universe')
 ---
 ...
 session.su('uniuser')
@@ -291,7 +291,7 @@ session.su('admin')
 box.schema.user.create('someuser')
 ---
 ...
-box.schema.user.grant('someuser', 'read, write, execute', 'universe')
+box.schema.user.grant('someuser', 'read, write, execute, create', 'universe')
 ---
 ...
 session.su('someuser')
diff --git a/test/box/access_misc.test.lua b/test/box/access_misc.test.lua
index 7dd796f62..b9fccb44e 100644
--- a/test/box/access_misc.test.lua
+++ b/test/box/access_misc.test.lua
@@ -86,7 +86,7 @@ s:select()
 -- and create this user session
 --
 box.schema.user.create('uniuser')
-box.schema.user.grant('uniuser', 'read, write, execute', 'universe')
+box.schema.user.grant('uniuser', 'read, write, execute, create', 'universe')
 session.su('uniuser')
 uid = session.uid()
 --
@@ -123,7 +123,7 @@ box.schema.func.create('uniuser_func')
 
 session.su('admin')
 box.schema.user.create('someuser')
-box.schema.user.grant('someuser', 'read, write, execute', 'universe')
+box.schema.user.grant('someuser', 'read, write, execute, create', 'universe')
 session.su('someuser')
 --
 -- Check drop objects of another user
diff --git a/test/box/call.result b/test/box/call.result
index 40d7ef952..349e5ce40 100644
--- a/test/box/call.result
+++ b/test/box/call.result
@@ -1,4 +1,4 @@
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.user.grant('guest', 'execute', 'universe')
 ---
 ...
 conn = require('net.box').connect(box.cfg.listen)
@@ -748,6 +748,6 @@ conn:close()
 require('msgpack').cfg { encode_sparse_safe = sparse_safe }
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'universe')
 ---
 ...
diff --git a/test/box/call.test.lua b/test/box/call.test.lua
index 0cc3b8fe6..a74aac4d1 100644
--- a/test/box/call.test.lua
+++ b/test/box/call.test.lua
@@ -1,4 +1,4 @@
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.user.grant('guest', 'execute', 'universe')
 
 conn = require('net.box').connect(box.cfg.listen)
 conn:ping()
@@ -240,4 +240,4 @@ conn:call_16("return_sparse4")
 conn:close()
 require('msgpack').cfg { encode_sparse_safe = sparse_safe }
 
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'universe')
diff --git a/test/box/errinj.result b/test/box/errinj.result
index 54b6d578f..d89c82247 100644
--- a/test/box/errinj.result
+++ b/test/box/errinj.result
@@ -784,7 +784,7 @@ test_run:cmd('setopt delimiter ""');
 - true
 ...
 -- Port_dump can fail.
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.user.grant('guest', 'read', 'space', '_space')
 ---
 ...
 cn = net_box.connect(box.cfg.listen)
@@ -816,7 +816,7 @@ errinj.set('ERRINJ_PORT_DUMP', false)
 cn:close()
 ---
 ...
-box.schema.user.revoke('guest', 'read, write, execute', 'universe')
+box.schema.user.revoke('guest', 'read', 'space', '_space')
 ---
 ...
 run()
@@ -1069,15 +1069,15 @@ s:drop()
 -- gh-3255: iproto can crash and discard responses, if a network
 -- is saturated, and DML yields too long on commit.
 --
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 s = box.schema.space.create('test')
 ---
 ...
 _ = s:create_index('pk')
 ---
 ...
+box.schema.user.grant('guest', 'read,write,alter', 'space', 'test')
+---
+...
 c = net_box.connect(box.cfg.listen)
 ---
 ...
@@ -1108,12 +1108,21 @@ s:drop()
 -- gh-3325: do not cancel already sent requests, when a schema
 -- change is detected.
 --
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
 s = box.schema.create_space('test')
 ---
 ...
 pk = s:create_index('pk')
 ---
 ...
+box.schema.user.grant('guest', 'read,write,alter', 'space', 'test')
+---
+...
+box.schema.user.grant('guest', 'write', 'space', '_index')
+---
+...
 s:replace{1, 1}
 ---
 - [1, 1]
@@ -1164,6 +1173,9 @@ cn:close()
 s:drop()
 ---
 ...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
 --
 -- If message memory pool is used up, stop the connection, until
 -- the pool has free memory.
@@ -1189,6 +1201,12 @@ function long_poll_f()
 end;
 ---
 ...
+box.schema.func.create('long_poll_f');
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'long_poll_f');
+---
+...
 test_run:cmd('setopt delimiter ""');
 ---
 - true
@@ -1250,7 +1268,7 @@ while finished ~= 2 do fiber.sleep(0.01) end
 cn:close()
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute','universe')
+box.schema.user.revoke('guest', 'execute', 'function', 'long_poll_f')
 ---
 ...
 --
diff --git a/test/box/errinj.test.lua b/test/box/errinj.test.lua
index 185ae19fc..5046159b5 100644
--- a/test/box/errinj.test.lua
+++ b/test/box/errinj.test.lua
@@ -245,7 +245,7 @@ test_run:cmd('setopt delimiter ""');
 
 -- Port_dump can fail.
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.user.grant('guest', 'read', 'space', '_space')
 
 cn = net_box.connect(box.cfg.listen)
 cn:ping()
@@ -255,7 +255,7 @@ assert(not ok)
 assert(string.match(tostring(ret), 'Failed to allocate'))
 errinj.set('ERRINJ_PORT_DUMP', false)
 cn:close()
-box.schema.user.revoke('guest', 'read, write, execute', 'universe')
+box.schema.user.revoke('guest', 'read', 'space', '_space')
 
 run()
 ch:get()
@@ -350,10 +350,9 @@ s:drop()
 -- is saturated, and DML yields too long on commit.
 --
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 s = box.schema.space.create('test')
 _ = s:create_index('pk')
-
+box.schema.user.grant('guest', 'read,write,alter', 'space', 'test')
 c = net_box.connect(box.cfg.listen)
 
 ch = fiber.channel(200)
@@ -369,8 +368,14 @@ s:drop()
 -- gh-3325: do not cancel already sent requests, when a schema
 -- change is detected.
 --
+
+box.schema.user.grant('guest', 'execute', 'universe')
+
 s = box.schema.create_space('test')
 pk = s:create_index('pk')
+
+box.schema.user.grant('guest', 'read,write,alter', 'space', 'test')
+box.schema.user.grant('guest', 'write', 'space', '_index')
 s:replace{1, 1}
 cn = net_box.connect(box.cfg.listen)
 errinj.set("ERRINJ_WAL_DELAY", true)
@@ -388,6 +393,7 @@ while ok == nil do fiber.sleep(0.01) end
 ok, err
 cn:close()
 s:drop()
+box.schema.user.revoke('guest', 'execute', 'universe')
 
 --
 -- If message memory pool is used up, stop the connection, until
@@ -403,6 +409,10 @@ function long_poll_f()
     while not continue do fiber.sleep(0.01) end
     finished = finished + 1
 end;
+
+box.schema.func.create('long_poll_f');
+box.schema.user.grant('guest', 'execute', 'function', 'long_poll_f');
+
 test_run:cmd('setopt delimiter ""');
 cn = net_box.connect(box.cfg.listen)
 function long_poll() cn:call('long_poll_f') end
@@ -428,8 +438,7 @@ errinj.set("ERRINJ_TESTING", false)
 while finished ~= 2 do fiber.sleep(0.01) end
 cn:close()
 
-box.schema.user.revoke('guest', 'read,write,execute','universe')
-
+box.schema.user.revoke('guest', 'execute', 'function', 'long_poll_f')
 --
 -- gh-3289: drop/truncate leaves the space in inconsistent
 -- state if WAL write fails.
diff --git a/test/box/net.box.result b/test/box/net.box.result
index d521534fe..2cfddba72 100644
--- a/test/box/net.box.result
+++ b/test/box/net.box.result
@@ -203,6 +203,9 @@ remote.self:eval('!invalid expression')
 ---
 - error: '[string "return !invalid expression"]:1: unexpected symbol near ''!'''
 ...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
 --
 -- gh-822: net.box.call should roll back local transaction on error
 --
@@ -265,10 +268,10 @@ test_run:cmd("setopt delimiter ''");
 box.space.gh822:drop()
 ---
 ...
-box.schema.user.revoke('guest','execute','universe')
+box.schema.user.grant('guest', 'read,write', 'space', 'net_box_test_space')
 ---
 ...
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest', 'execute', 'universe')
 ---
 ...
 cn:close()
@@ -496,6 +499,15 @@ cn.space.net_box_test_space:get(354)
 - [354, 1, 2, 4]
 ...
 -- reconnects after errors
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
+box.schema.func.create('test_foo')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'test_foo')
+---
+...
 -- -- 1. no reconnect
 x_fatal(cn)
 ---
@@ -593,6 +605,12 @@ type(fiber.create(function() fiber.sleep(.5) x_fatal(cn) end))
 function pause() fiber.sleep(10) return true end
 ---
 ...
+box.schema.func.create('pause')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'pause')
+---
+...
 cn:call('pause')
 ---
 - error: Peer closed
@@ -601,6 +619,9 @@ cn:call('test_foo', {'a', 'b', 'c'})
 ---
 - [[{'a': 1}], [{'b': 2}], 'c']
 ...
+box.schema.func.drop('pause')
+---
+...
 -- call
 remote.self:call('test_foo', {'a', 'b', 'c'})
 ---
@@ -612,6 +633,15 @@ cn:call('test_foo', {'a', 'b', 'c'})
 ---
 - [[{'a': 1}], [{'b': 2}], 'c']
 ...
+box.schema.func.drop('test_foo')
+---
+...
+box.schema.func.create('long_rep')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'long_rep')
+---
+...
 -- long replies
 function long_rep() return { 1,  string.rep('a', 5000) } end
 ---
@@ -641,6 +671,9 @@ res[2] == string.rep('a', 50000)
 ---
 - true
 ...
+box.schema.func.drop('long_rep')
+---
+...
 -- a.b.c.d
 u = '84F7BCFA-079C-46CC-98B4-F0C821BE833E'
 ---
@@ -654,6 +687,15 @@ X.X = X
 function X.fn(x,y) return y or x end
 ---
 ...
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
+cn:close()
+---
+...
+cn = remote.connect(LISTEN.host, LISTEN.service)
+---
+...
 cn:call('X.fn', {u})
 ---
 - 84F7BCFA-079C-46CC-98B4-F0C821BE833E
@@ -666,6 +708,12 @@ cn:call('X.X.X.X:fn', {u})
 ---
 - 84F7BCFA-079C-46CC-98B4-F0C821BE833E
 ...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
+cn:close()
+---
+...
 -- auth
 cn = remote.connect(LISTEN.host, LISTEN.service, { user = 'netbox', password = '123', wait_connected = true })
 ---
@@ -685,7 +733,10 @@ cn.state
 box.schema.user.create('netbox', { password  = 'test' })
 ---
 ...
-box.schema.user.grant('netbox', 'read, write, execute', 'universe');
+box.schema.user.grant('netbox', 'read,write', 'space', 'net_box_test_space')
+---
+...
+box.schema.user.grant('netbox', 'execute', 'universe')
 ---
 ...
 cn = remote.connect(LISTEN.host, LISTEN.service, { user = 'netbox', password = 'test' })
@@ -1089,6 +1140,15 @@ cn:close()
 cn = remote.connect(LISTEN.host, LISTEN.service)
 ---
 ...
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
+cn:close()
+---
+...
+cn = remote.connect(LISTEN.host, LISTEN.service)
+---
+...
 cn:eval('return true')
 ---
 - true
@@ -1112,6 +1172,9 @@ remote.self.eval('return true')
 ---
 - error: 'Use remote:eval(...) instead of remote.eval(...):'
 ...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
 -- uri as the first argument
 uri = string.format('%s:%s@%s:%s', 'netbox', 'test', LISTEN.host, LISTEN.service)
 ---
@@ -1156,13 +1219,16 @@ cn:ping()
 cn:close()
 ---
 ...
-box.schema.user.revoke('netbox', 'read, write, execute', 'universe');
----
-...
 box.schema.user.drop('netbox')
 ---
 ...
 -- #594: bad argument #1 to 'setmetatable' (table expected, got number)
+box.schema.func.create('dostring')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'dostring')
+---
+...
 test_run:cmd("setopt delimiter ';'")
 ---
 - true
@@ -1182,6 +1248,9 @@ test_run:cmd("setopt delimiter ''");
 gh594()
 ---
 ...
+box.schema.func.drop('dostring')
+---
+...
 -- #636: Reload schema on demand
 sp = box.schema.space.create('test_old')
 ---
@@ -1193,6 +1262,9 @@ sp:insert{1, 2, 3}
 ---
 - [1, 2, 3]
 ...
+box.schema.user.grant('guest', 'read', 'space', 'test_old')
+---
+...
 con = remote.new(box.cfg.listen)
 ---
 ...
@@ -1218,6 +1290,9 @@ sp:insert{2, 3, 4}
 ---
 - [2, 3, 4]
 ...
+box.schema.user.grant('guest', 'read', 'space', 'test')
+---
+...
 con.space.test == nil
 ---
 - true
@@ -1248,6 +1323,9 @@ file_log:seek(0, 'SEEK_END') ~= 0
 ---
 - true
 ...
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
 test_run:cmd("setopt delimiter ';'")
 ---
 - true
@@ -1269,6 +1347,9 @@ test_run:grep_log("default", "ER_NO_SUCH_PROC")
 ---
 - ER_NO_SUCH_PROC
 ...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
 -- gh-983 selecting a lot of data crashes the server or hangs the
 -- connection
 -- gh-983 test case: iproto connection selecting a lot of data
@@ -1284,6 +1365,9 @@ data1k = "aaaabbbbccccddddeeeeffffgggghhhhaaaabbbbccccddddeeeeffffgggghhhhaaaabb
 for i = 0,10000 do box.space.test:insert{i, data1k} end
 ---
 ...
+box.schema.user.grant('guest', 'read', 'space', 'test')
+---
+...
 net = require('net.box')
 ---
 ...
@@ -1309,6 +1393,9 @@ _ = box.space.test:create_index('covering', {type = 'TREE', parts = {1,'unsigned
 _ = box.space.test:insert{1, 2, "string"}
 ---
 ...
+box.schema.user.grant('guest', 'read,write', 'space', 'test')
+---
+...
 c = net:connect(box.cfg.listen)
 ---
 ...
@@ -1365,6 +1452,9 @@ box.space.test:drop()
 function echo(...) return ... end
 ---
 ...
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
 c = net.connect(box.cfg.listen)
 ---
 ...
@@ -1404,6 +1494,9 @@ c:eval('return echo(...)', 42)
 c:close()
 ---
 ...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
 --
 -- gh-2195 export pure msgpack from net.box
 --
@@ -1413,6 +1506,12 @@ space = box.schema.space.create('test')
 _ = box.space.test:create_index('primary')
 ---
 ...
+box.schema.user.grant('guest', 'read,write', 'space', 'test')
+---
+...
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
 c = net.connect(box.cfg.listen)
 ---
 ...
@@ -1648,6 +1747,9 @@ c:close()
 space:drop()
 ---
 ...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
 -- gh-1904 net.box hangs in :close() if a fiber was cancelled
 -- while blocked in :_wait_state() in :_request()
 options = {user = 'netbox', password = 'badpass', wait_connected = false, reconnect_after = 0.01}
@@ -1665,6 +1767,9 @@ fiber.sleep(0.01)
 f:cancel(); c:close()
 ---
 ...
+box.schema.user.grant('guest', 'read', 'space', '_schema')
+---
+...
 -- check for on_schema_reload callback
 test_run:cmd("setopt delimiter ';'")
 ---
@@ -1717,7 +1822,7 @@ test_run:cmd("setopt delimiter ''");
 ---
 - true
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'read', 'space', '_schema')
 ---
 ...
 -- Tarantool < 1.7.1 compatibility (gh-1533)
@@ -1806,7 +1911,7 @@ space ~= nil
 _ = box.space.test:create_index('primary')
 ---
 ...
-box.schema.user.grant('guest','read,write,execute','space', 'test')
+box.schema.user.grant('guest', 'read', 'space', 'test')
 ---
 ...
 c = net.connect(box.cfg.listen)
@@ -1885,7 +1990,7 @@ c:close()
 --
 -- gh-2642: box.session.type()
 --
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest','execute','universe')
 ---
 ...
 c = net.connect(box.cfg.listen)
@@ -1898,6 +2003,9 @@ c:call("box.session.type")
 c:close()
 ---
 ...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
 --
 -- On_connect/disconnect triggers.
 --
@@ -1977,6 +2085,9 @@ space:drop()
 space = box.schema.space.create('test')
 ---
 ...
+box.schema.user.grant('guest', 'read', 'space', 'test')
+---
+...
 c = net.connect(box.cfg.listen)
 ---
 ...
@@ -2010,16 +2121,28 @@ c.space.test.index.test_index ~= nil
 - true
 ...
 -- cleanup
-box.schema.user.revoke('guest','read,write,execute','universe')
----
-...
 space:drop()
 ---
 ...
 --
 -- gh-946: long polling CALL blocks input
 --
-box.schema.user.grant('guest', 'execute', 'universe')
+box.schema.func.create('fast_call')
+---
+...
+box.schema.func.create('long_call')
+---
+...
+box.schema.func.create('wait_signal')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'fast_call')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'long_call')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'wait_signal')
 ---
 ...
 c = net.connect(box.cfg.listen)
@@ -2136,6 +2259,15 @@ disconnected -- true
 box.session.on_disconnect(nil, on_disconnect)
 ---
 ...
+box.schema.func.drop('long_call')
+---
+...
+box.schema.func.drop('fast_call')
+---
+...
+box.schema.func.drop('wait_signal')
+---
+...
 --
 -- gh-2666: check that netbox.call is not repeated on schema
 -- change.
@@ -2155,6 +2287,12 @@ count = 0
 function create_space(name) count = count + 1 box.schema.create_space(name) return true end
 ---
 ...
+box.schema.func.create('create_space')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'create_space')
+---
+...
 c = net.connect(box.cfg.listen)
 ---
 ...
@@ -2203,6 +2341,9 @@ box.schema.user.revoke('guest', 'create', 'universe')
 c:close()
 ---
 ...
+box.schema.func.drop('create_space')
+---
+...
 --
 -- gh-3164: netbox connection is not closed and garbage collected
 -- ever, if reconnect_after is set.
@@ -2311,7 +2452,7 @@ weak.c
 -- binary or text protocol, and netbox could not be created from
 -- existing socket.
 --
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest', 'execute', 'universe')
 ---
 ...
 urilib = require('uri')
@@ -2350,6 +2491,9 @@ c:call('kek', {300})
 s = box.schema.create_space('test')
 ---
 ...
+box.schema.user.grant('guest', 'read,write', 'space', 'test')
+---
+...
 pk = s:create_index('pk')
 ---
 ...
@@ -2404,7 +2548,7 @@ while c.state ~= 'error_reconnect' do fiber.sleep(0.01) end
 c:close()
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'universe')
 ---
 ...
 c.state
@@ -2420,7 +2564,7 @@ c = nil
 space = box.schema.create_space('test')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.user.grant('guest', 'read', 'space', 'test')
 ---
 ...
 _ = space:create_index('pk')
@@ -2444,6 +2588,15 @@ space:drop()
 space = box.schema.create_space('test')
 ---
 ...
+c:close()
+---
+...
+box.schema.user.grant('guest', 'read', 'space', 'test')
+---
+...
+c = net:connect(box.cfg.listen)
+---
+...
 box.internal.collation.create('test', 'ICU', 'ru-RU')
 ---
 ...
@@ -2482,6 +2635,12 @@ c = nil
 cond = nil
 ---
 ...
+box.schema.func.create('long_function')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'long_function')
+---
+...
 function long_function(...) cond = fiber.cond() cond:wait() return ... end
 ---
 ...
@@ -2589,6 +2748,15 @@ ret
 ---
 - [1, 2, 3]
 ...
+c:close()
+---
+...
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
+c = net:connect(box.cfg.listen)
+---
+...
 future = c:eval('return long_function(...)', {1, 2, 3}, {is_async = true})
 ---
 ...
@@ -2609,6 +2777,15 @@ future:wait_result(100)
 ---
 - [1, 2, 3]
 ...
+c:close()
+---
+...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
+c = net:connect(box.cfg.listen)
+---
+...
 --
 -- Ensure the request is garbage collected both if is not used and
 -- if is.
@@ -2716,6 +2893,15 @@ ret
 --
 -- Test space methods.
 --
+c:close()
+---
+...
+box.schema.user.grant('guest', 'read,write', 'space', 'test')
+---
+...
+c = net:connect(box.cfg.listen)
+---
+...
 future = c.space.test:select({1}, {is_async = true})
 ---
 ...
@@ -2822,6 +3008,15 @@ future:wait_result(100)
 ---
 - [5, 6]
 ...
+c:close()
+---
+...
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
+c = net:connect(box.cfg.listen)
+---
+...
 future = c.space.test.index.pk:count({3}, {is_async = true})
 ---
 ...
@@ -2829,6 +3024,15 @@ future:wait_result(100)
 ---
 - 1
 ...
+c:close()
+---
+...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
+c = net:connect(box.cfg.listen)
+---
+...
 future = c.space.test.index.pk:delete({3}, {is_async = true})
 ---
 ...
@@ -2997,12 +3201,30 @@ result
 ---
 - {48: [1, 2, 3]}
 ...
+box.schema.func.drop('long_function')
+---
+...
 --
 -- Test async schema version change.
 --
 function change_schema(i) local tmp = box.schema.create_space('test'..i) return 'ok' end
 ---
 ...
+box.schema.func.create('change_schema')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'change_schema')
+---
+...
+box.schema.user.grant('guest', 'write', 'space', '_schema')
+---
+...
+box.schema.user.grant('guest', 'read,write', 'space', '_space')
+---
+...
+box.schema.user.grant('guest', 'create', 'space')
+---
+...
 future1 = c:call('change_schema', {'1'}, {is_async = true})
 ---
 ...
@@ -3039,6 +3261,9 @@ box.space.test2:drop()
 box.space.test3:drop()
 ---
 ...
+box.schema.func.drop('change_schema')
+---
+...
 --
 -- gh-3400: long-poll input discard must not touch event loop of
 -- a closed connection.
@@ -3090,6 +3315,12 @@ test_run:grep_log('default', 'too big packet size in the header') ~= nil
 ---
 - true
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'write', 'space', '_schema')
+---
+...
+box.schema.user.revoke('guest', 'read,write', 'space', '_space')
+---
+...
+box.schema.user.revoke('guest', 'create', 'space')
 ---
 ...
diff --git a/test/box/net.box.test.lua b/test/box/net.box.test.lua
index 623475d41..bf575c6bc 100644
--- a/test/box/net.box.test.lua
+++ b/test/box/net.box.test.lua
@@ -31,7 +31,6 @@ log.info("ping is done")
 
 cn:ping()
 
-
 -- check permissions
 cn:call('unexists_procedure')
 function test_foo(a,b,c) return { {{ [a] = 1 }}, {{ [b] = 2 }}, c } end
@@ -73,6 +72,8 @@ remote.self:eval('error("exception")')
 remote.self:eval('box.error(0)')
 remote.self:eval('!invalid expression')
 
+box.schema.user.revoke('guest', 'execute', 'universe')
+
 --
 -- gh-822: net.box.call should roll back local transaction on error
 --
@@ -113,8 +114,9 @@ rollback_on_eval_error();
 test_run:cmd("setopt delimiter ''");
 box.space.gh822:drop()
 
-box.schema.user.revoke('guest','execute','universe')
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest', 'read,write', 'space', 'net_box_test_space')
+box.schema.user.grant('guest', 'execute', 'universe')
+
 cn:close()
 cn = remote.connect(box.cfg.listen)
 
@@ -189,6 +191,10 @@ cn.space.net_box_test_space:get(354)
 
 -- reconnects after errors
 
+box.schema.user.revoke('guest', 'execute', 'universe')
+box.schema.func.create('test_foo')
+box.schema.user.grant('guest', 'execute', 'function', 'test_foo')
+
 -- -- 1. no reconnect
 x_fatal(cn)
 cn.state
@@ -224,13 +230,19 @@ cn1:close()
 type(fiber.create(function() fiber.sleep(.5) x_fatal(cn) end))
 function pause() fiber.sleep(10) return true end
 
+box.schema.func.create('pause')
+box.schema.user.grant('guest', 'execute', 'function', 'pause')
 cn:call('pause')
 cn:call('test_foo', {'a', 'b', 'c'})
-
+box.schema.func.drop('pause')
 
 -- call
 remote.self:call('test_foo', {'a', 'b', 'c'})
 cn:call('test_foo', {'a', 'b', 'c'})
+box.schema.func.drop('test_foo')
+
+box.schema.func.create('long_rep')
+box.schema.user.grant('guest', 'execute', 'function', 'long_rep')
 
 -- long replies
 function long_rep() return { 1,  string.rep('a', 5000) } end
@@ -243,14 +255,21 @@ res = cn:call('long_rep')
 res[1] == 1
 res[2] == string.rep('a', 50000)
 
+box.schema.func.drop('long_rep')
+
 -- a.b.c.d
 u = '84F7BCFA-079C-46CC-98B4-F0C821BE833E'
 X = {}
 X.X = X
 function X.fn(x,y) return y or x end
+box.schema.user.grant('guest', 'execute', 'universe')
+cn:close()
+cn = remote.connect(LISTEN.host, LISTEN.service)
 cn:call('X.fn', {u})
 cn:call('X.X.X.X.X.X.X.fn', {u})
 cn:call('X.X.X.X:fn', {u})
+box.schema.user.revoke('guest', 'execute', 'universe')
+cn:close()
 
 -- auth
 
@@ -259,9 +278,10 @@ cn:is_connected()
 cn.error
 cn.state
 
-box.schema.user.create('netbox', { password  = 'test' })
-box.schema.user.grant('netbox', 'read, write, execute', 'universe');
 
+box.schema.user.create('netbox', { password  = 'test' })
+box.schema.user.grant('netbox', 'read,write', 'space', 'net_box_test_space')
+box.schema.user.grant('netbox', 'execute', 'universe')
 cn = remote.connect(LISTEN.host, LISTEN.service, { user = 'netbox', password = 'test' })
 cn.state
 cn.error
@@ -404,6 +424,9 @@ cn:close()
 -- #544 usage for remote[point]method
 cn = remote.connect(LISTEN.host, LISTEN.service)
 
+box.schema.user.grant('guest', 'execute', 'universe')
+cn:close()
+cn = remote.connect(LISTEN.host, LISTEN.service)
 cn:eval('return true')
 cn.eval('return true')
 
@@ -413,7 +436,7 @@ cn:close()
 
 remote.self:eval('return true')
 remote.self.eval('return true')
-
+box.schema.user.revoke('guest', 'execute', 'universe')
 
 -- uri as the first argument
 uri = string.format('%s:%s@%s:%s', 'netbox', 'test', LISTEN.host, LISTEN.service)
@@ -432,10 +455,11 @@ cn = remote.new(uri, { user = 'netbox', password = 'test' })
 cn:ping()
 cn:close()
 
-box.schema.user.revoke('netbox', 'read, write, execute', 'universe');
 box.schema.user.drop('netbox')
 
 -- #594: bad argument #1 to 'setmetatable' (table expected, got number)
+box.schema.func.create('dostring')
+box.schema.user.grant('guest', 'execute', 'function', 'dostring')
 test_run:cmd("setopt delimiter ';'")
 function gh594()
     local cn = remote.connect(box.cfg.listen)
@@ -445,12 +469,15 @@ function gh594()
 end;
 test_run:cmd("setopt delimiter ''");
 gh594()
+box.schema.func.drop('dostring')
+
 
 -- #636: Reload schema on demand
 sp = box.schema.space.create('test_old')
 _ = sp:create_index('primary')
 sp:insert{1, 2, 3}
 
+box.schema.user.grant('guest', 'read', 'space', 'test_old')
 con = remote.new(box.cfg.listen)
 con:ping()
 con.space.test_old:select{}
@@ -460,6 +487,8 @@ sp = box.schema.space.create('test')
 _ = sp:create_index('primary')
 sp:insert{2, 3, 4}
 
+box.schema.user.grant('guest', 'read', 'space', 'test')
+
 con.space.test == nil
 con:reload_schema()
 con.space.test:select{}
@@ -472,6 +501,7 @@ name = string.match(arg[0], "([^,]+)%.lua")
 file_log = require('fio').open(name .. '.log', {'O_RDONLY', 'O_NONBLOCK'})
 file_log:seek(0, 'SEEK_END') ~= 0
 
+box.schema.user.grant('guest', 'execute', 'universe')
 test_run:cmd("setopt delimiter ';'")
 
 _ = fiber.create(
@@ -483,6 +513,7 @@ _ = fiber.create(
 );
 test_run:cmd("setopt delimiter ''");
 test_run:grep_log("default", "ER_NO_SUCH_PROC")
+box.schema.user.revoke('guest', 'execute', 'universe')
 
 -- gh-983 selecting a lot of data crashes the server or hangs the
 -- connection
@@ -495,6 +526,7 @@ data1k = "aaaabbbbccccddddeeeeffffgggghhhhaaaabbbbccccddddeeeeffffgggghhhhaaaabb
 
 for i = 0,10000 do box.space.test:insert{i, data1k} end
 
+box.schema.user.grant('guest', 'read', 'space', 'test')
 net = require('net.box')
 c = net:connect(box.cfg.listen)
 r = c.space.test:select(nil, {limit=5000})
@@ -505,6 +537,7 @@ _ = box.schema.space.create('test')
 _ = box.space.test:create_index('primary', {type = 'TREE', parts = {1,'unsigned'}})
 _ = box.space.test:create_index('covering', {type = 'TREE', parts = {1,'unsigned',3,'string',2,'unsigned'}})
 _ = box.space.test:insert{1, 2, "string"}
+box.schema.user.grant('guest', 'read,write', 'space', 'test')
 c = net:connect(box.cfg.listen)
 c.space.test:select{}
 c.space.test:upsert({1, 2, 'nothing'}, {{'+', 2, 1}}) -- common update
@@ -522,6 +555,7 @@ box.space.test:drop()
 
 -- CALL vs CALL_16 in connect options
 function echo(...) return ... end
+box.schema.user.grant('guest', 'execute', 'universe')
 c = net.connect(box.cfg.listen)
 c:call('echo', {42})
 c:eval('return echo(...)', {42})
@@ -533,6 +567,7 @@ c = net.connect(box.cfg.listen, {call_16 = true})
 c:call('echo', 42)
 c:eval('return echo(...)', 42)
 c:close()
+box.schema.user.revoke('guest', 'execute', 'universe')
 
 --
 -- gh-2195 export pure msgpack from net.box
@@ -540,6 +575,8 @@ c:close()
 
 space = box.schema.space.create('test')
 _ = box.space.test:create_index('primary')
+box.schema.user.grant('guest', 'read,write', 'space', 'test')
+box.schema.user.grant('guest', 'execute', 'universe')
 c = net.connect(box.cfg.listen)
 ibuf = require('buffer').ibuf()
 
@@ -626,6 +663,7 @@ ibuf.rpos == rpos, ibuf.wpos == wpos
 ibuf = nil
 c:close()
 space:drop()
+box.schema.user.revoke('guest', 'execute', 'universe')
 
 -- gh-1904 net.box hangs in :close() if a fiber was cancelled
 -- while blocked in :_wait_state() in :_request()
@@ -635,6 +673,8 @@ f = fiber.create(function() c:call("") end)
 fiber.sleep(0.01)
 f:cancel(); c:close()
 
+box.schema.user.grant('guest', 'read', 'space', '_schema')
+
 -- check for on_schema_reload callback
 test_run:cmd("setopt delimiter ';'")
 do
@@ -676,7 +716,7 @@ do
 end;
 test_run:cmd("setopt delimiter ''");
 
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'read', 'space', '_schema')
 
 -- Tarantool < 1.7.1 compatibility (gh-1533)
 c = net.new(box.cfg.listen)
@@ -722,7 +762,7 @@ test_run:cmd("clear filter")
 space = box.schema.space.create('test', {format={{name="id", type="unsigned"}}})
 space ~= nil
 _ = box.space.test:create_index('primary')
-box.schema.user.grant('guest','read,write,execute','space', 'test')
+box.schema.user.grant('guest', 'read', 'space', 'test')
 
 c = net.connect(box.cfg.listen)
 
@@ -762,10 +802,12 @@ c:close()
 -- gh-2642: box.session.type()
 --
 
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest','execute','universe')
 c = net.connect(box.cfg.listen)
 c:call("box.session.type")
 c:close()
+box.schema.user.revoke('guest', 'execute', 'universe')
+
 
 --
 -- On_connect/disconnect triggers.
@@ -795,6 +837,7 @@ test_run:cmd('stop server connecter')
 --
 space:drop()
 space = box.schema.space.create('test')
+box.schema.user.grant('guest', 'read', 'space', 'test')
 c = net.connect(box.cfg.listen)
 cspace = c.space.test
 space.index.test_index == nil
@@ -806,15 +849,18 @@ cspace.index.test_index ~= nil
 c.space.test.index.test_index ~= nil
 
 -- cleanup
-box.schema.user.revoke('guest','read,write,execute','universe')
 
 space:drop()
 
 --
 -- gh-946: long polling CALL blocks input
 --
-box.schema.user.grant('guest', 'execute', 'universe')
-
+box.schema.func.create('fast_call')
+box.schema.func.create('long_call')
+box.schema.func.create('wait_signal')
+box.schema.user.grant('guest', 'execute', 'function', 'fast_call')
+box.schema.user.grant('guest', 'execute', 'function', 'long_call')
+box.schema.user.grant('guest', 'execute', 'function', 'wait_signal')
 c = net.connect(box.cfg.listen)
 
 N = 100
@@ -871,6 +917,9 @@ disconnected -- true
 
 box.session.on_disconnect(nil, on_disconnect)
 
+box.schema.func.drop('long_call')
+box.schema.func.drop('fast_call')
+box.schema.func.drop('wait_signal')
 --
 -- gh-2666: check that netbox.call is not repeated on schema
 -- change.
@@ -880,6 +929,8 @@ box.schema.user.grant('guest', 'write', 'space', '_schema')
 box.schema.user.grant('guest', 'create', 'universe')
 count = 0
 function create_space(name) count = count + 1 box.schema.create_space(name) return true end
+box.schema.func.create('create_space')
+box.schema.user.grant('guest', 'execute', 'function', 'create_space')
 c = net.connect(box.cfg.listen)
 c:call('create_space', {'test1'})
 count
@@ -894,6 +945,7 @@ box.schema.user.revoke('guest', 'write', 'space', '_space')
 box.schema.user.revoke('guest', 'write', 'space', '_schema')
 box.schema.user.revoke('guest', 'create', 'universe')
 c:close()
+box.schema.func.drop('create_space')
 
 --
 -- gh-3164: netbox connection is not closed and garbage collected
@@ -946,7 +998,7 @@ weak.c
 -- binary or text protocol, and netbox could not be created from
 -- existing socket.
 --
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest', 'execute', 'universe')
 urilib = require('uri')
 uri = urilib.parse(tostring(box.cfg.listen))
 s, greeting = net.establish_connection(uri.host, uri.service)
@@ -959,6 +1011,7 @@ c:eval('a = 200')
 a
 c:call('kek', {300})
 s = box.schema.create_space('test')
+box.schema.user.grant('guest', 'read,write', 'space', 'test')
 pk = s:create_index('pk')
 c:reload_schema()
 c.space.test:replace{1}
@@ -983,7 +1036,7 @@ c = net.connect('localhost:33333', {reconnect_after = 0.1, wait_connected = fals
 while c.state ~= 'error_reconnect' do fiber.sleep(0.01) end
 c:close()
 
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'universe')
 c.state
 c = nil
 
@@ -991,7 +1044,7 @@ c = nil
 -- gh-3256 net.box is_nullable and collation options output
 --
 space = box.schema.create_space('test')
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.user.grant('guest', 'read', 'space', 'test')
 _ = space:create_index('pk')
 _ = space:create_index('sk', {parts = {{2, 'unsigned', is_nullable = true}}})
 c = net:connect(box.cfg.listen)
@@ -999,6 +1052,9 @@ c.space.test.index.sk.parts
 space:drop()
 
 space = box.schema.create_space('test')
+c:close()
+box.schema.user.grant('guest', 'read', 'space', 'test')
+c = net:connect(box.cfg.listen)
 box.internal.collation.create('test', 'ICU', 'ru-RU')
 _ = space:create_index('sk', { type = 'tree', parts = {{1, 'str', collation = 'test'}}, unique = true })
 c:reload_schema()
@@ -1013,6 +1069,8 @@ c = nil
 -- gh-3107: fiber-async netbox.
 --
 cond = nil
+box.schema.func.create('long_function')
+box.schema.user.grant('guest', 'execute', 'function', 'long_function')
 function long_function(...) cond = fiber.cond() cond:wait() return ... end
 function finalize_long() while not cond do fiber.sleep(0.01) end cond:signal() cond = nil end
 s = box.schema.create_space('test')
@@ -1050,13 +1108,19 @@ _ = fiber.create(function() ret = c:call('long_function', {1, 2, 3}, {is_async =
 finalize_long()
 while not ret do fiber.sleep(0.01) end
 ret
-
+c:close()
+box.schema.user.grant('guest', 'execute', 'universe')
+c = net:connect(box.cfg.listen)
 future = c:eval('return long_function(...)', {1, 2, 3}, {is_async = true})
 future:result()
 future:wait_result(0.01) -- Must fail on timeout.
 finalize_long()
 future:wait_result(100)
 
+c:close()
+box.schema.user.revoke('guest', 'execute', 'universe')
+c = net:connect(box.cfg.listen)
+
 --
 -- Ensure the request is garbage collected both if is not used and
 -- if is.
@@ -1095,6 +1159,9 @@ ret
 --
 -- Test space methods.
 --
+c:close()
+box.schema.user.grant('guest', 'read,write', 'space', 'test')
+c = net:connect(box.cfg.listen)
 future = c.space.test:select({1}, {is_async = true})
 ret = future:wait_result(100)
 ret
@@ -1128,8 +1195,14 @@ future = c.space.test.index.pk:min({}, {is_async = true})
 future:wait_result(100)
 future = c.space.test.index.pk:max({}, {is_async = true})
 future:wait_result(100)
+c:close()
+box.schema.user.grant('guest', 'execute', 'universe')
+c = net:connect(box.cfg.listen)
 future = c.space.test.index.pk:count({3}, {is_async = true})
 future:wait_result(100)
+c:close()
+box.schema.user.revoke('guest', 'execute', 'universe')
+c = net:connect(box.cfg.listen)
 future = c.space.test.index.pk:delete({3}, {is_async = true})
 future:wait_result(100)
 s:get{3}
@@ -1194,10 +1267,17 @@ future:wait_result(100)
 result, ibuf.rpos = msgpack.decode_unchecked(ibuf.rpos)
 result
 
+box.schema.func.drop('long_function')
+
 --
 -- Test async schema version change.
 --
 function change_schema(i) local tmp = box.schema.create_space('test'..i) return 'ok' end
+box.schema.func.create('change_schema')
+box.schema.user.grant('guest', 'execute', 'function', 'change_schema')
+box.schema.user.grant('guest', 'write', 'space', '_schema')
+box.schema.user.grant('guest', 'read,write', 'space', '_space')
+box.schema.user.grant('guest', 'create', 'space')
 future1 = c:call('change_schema', {'1'}, {is_async = true})
 future2 = c:call('change_schema', {'2'}, {is_async = true})
 future3 = c:call('change_schema', {'3'}, {is_async = true})
@@ -1210,6 +1290,7 @@ s:drop()
 box.space.test1:drop()
 box.space.test2:drop()
 box.space.test3:drop()
+box.schema.func.drop('change_schema')
 
 --
 -- gh-3400: long-poll input discard must not touch event loop of
@@ -1238,4 +1319,6 @@ c._transport.perform_request(nil, nil, 'inject', nil, nil, data)
 c:close()
 test_run:grep_log('default', 'too big packet size in the header') ~= nil
 
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'write', 'space', '_schema')
+box.schema.user.revoke('guest', 'read,write', 'space', '_space')
+box.schema.user.revoke('guest', 'create', 'space')
diff --git a/test/box/net_msg_max.result b/test/box/net_msg_max.result
index ccda2014e..85872b65f 100644
--- a/test/box/net_msg_max.result
+++ b/test/box/net_msg_max.result
@@ -7,7 +7,13 @@ fiber = require('fiber')
 net_box = require('net.box')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.user.grant('guest', 'read', 'space', '_space')
+---
+...
+box.schema.func.create('do_long_f')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'do_long_f')
 ---
 ...
 conn = net_box.connect(box.cfg.listen)
@@ -252,7 +258,10 @@ conn2:close()
 conn:close()
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.func.drop('do_long_f')
+---
+...
+box.schema.user.revoke('guest', 'read', 'space', '_space')
 ---
 ...
 box.cfg{readahead = old_readahead, net_msg_max = limit}
diff --git a/test/box/net_msg_max.test.lua b/test/box/net_msg_max.test.lua
index 13f7050ed..c793b0f1c 100644
--- a/test/box/net_msg_max.test.lua
+++ b/test/box/net_msg_max.test.lua
@@ -3,7 +3,10 @@ test_run = require('test_run').new()
 fiber = require('fiber')
 net_box = require('net.box')
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.user.grant('guest', 'read', 'space', '_space')
+box.schema.func.create('do_long_f')
+box.schema.user.grant('guest', 'execute', 'function', 'do_long_f')
+
 conn = net_box.connect(box.cfg.listen)
 conn2 = net_box.connect(box.cfg.listen)
 active = 0
@@ -79,6 +82,7 @@ wait_finished(run_max)
 --
 -- Test minimal iproto msg count.
 --
+
 box.cfg{net_msg_max = 2}
 conn:ping()
 #conn.space._space:select{} > 0
@@ -136,5 +140,6 @@ wait_finished(110)
 conn2:close()
 conn:close()
 
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.func.drop('do_long_f')
+box.schema.user.revoke('guest', 'read', 'space', '_space')
 box.cfg{readahead = old_readahead, net_msg_max = limit}
diff --git a/test/box/on_replace.result b/test/box/on_replace.result
index fcdb43794..20ffe6412 100644
--- a/test/box/on_replace.result
+++ b/test/box/on_replace.result
@@ -540,7 +540,7 @@ s:select()
 ---
 - []
 ...
-s:drop()
+s:drop() -- test_on_repl_ddl
 ---
 ...
 --
diff --git a/test/box/on_replace.test.lua b/test/box/on_replace.test.lua
index 802aaaf2a..8a9fd3678 100644
--- a/test/box/on_replace.test.lua
+++ b/test/box/on_replace.test.lua
@@ -202,7 +202,7 @@ s:replace({8, 9})
 t = s:on_replace(function () s.index.pk:rename('newname') end, t)
 s:replace({9, 10})
 s:select()
-s:drop()
+s:drop() -- test_on_repl_ddl
 
 --
 -- gh-3020: sub-statement rollback
diff --git a/test/box/protocol.result b/test/box/protocol.result
index 3e43663b1..e03186cc9 100644
--- a/test/box/protocol.result
+++ b/test/box/protocol.result
@@ -1,6 +1,3 @@
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 --------------------------------------------------------------------------------
 -- Test case for #273: IPROTO_ITERATOR ignored in network protocol
 --------------------------------------------------------------------------------
@@ -10,6 +7,9 @@ space = box.schema.space.create('tweedledum')
 index = space:create_index('primary', { type = 'tree'})
 ---
 ...
+box.schema.user.grant('guest', 'read', 'space', 'tweedledum')
+---
+...
 for i=1,5 do space:insert{i} end
 ---
 ...
@@ -51,6 +51,3 @@ conn:close()
 space:drop()
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
diff --git a/test/box/protocol.test.lua b/test/box/protocol.test.lua
index 5026d585e..805a43721 100644
--- a/test/box/protocol.test.lua
+++ b/test/box/protocol.test.lua
@@ -1,4 +1,3 @@
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 
 --------------------------------------------------------------------------------
 -- Test case for #273: IPROTO_ITERATOR ignored in network protocol
@@ -6,6 +5,9 @@ box.schema.user.grant('guest', 'read,write,execute', 'universe')
 
 space = box.schema.space.create('tweedledum')
 index = space:create_index('primary', { type = 'tree'})
+
+box.schema.user.grant('guest', 'read', 'space', 'tweedledum')
+
 for i=1,5 do space:insert{i} end
 
 LISTEN = require('uri').parse(box.cfg.listen)
@@ -18,4 +20,3 @@ conn.space[space.id]:select(3, { iterator = 'LT' })
 conn:close()
 
 space:drop()
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
diff --git a/test/box/push.result b/test/box/push.result
index 4ec7c546c..af730c1a7 100644
--- a/test/box/push.result
+++ b/test/box/push.result
@@ -42,7 +42,10 @@ test_run:cmd("setopt delimiter ''");
 netbox = require('net.box')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+box.schema.func.create('do_pushes')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'do_pushes')
 ---
 ...
 c = netbox.connect(box.cfg.listen)
@@ -78,6 +81,9 @@ finished = 0
 s = box.schema.create_space('test', {format = {{'field1', 'integer'}}})
 ---
 ...
+box.schema.user.grant('guest', 'write', 'space', 'test')
+---
+...
 pk = s:create_index('pk')
 ---
 ...
@@ -133,6 +139,12 @@ function dml_push_and_dml_f()
 end;
 ---
 ...
+box.schema.func.create('dml_push_and_dml');
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'dml_push_and_dml');
+---
+...
 -- At first check that a pushed message can be ignored in a binary
 -- protocol too.
 c:call('do_pushes', {300});
@@ -149,6 +161,8 @@ end;
 while finished ~= 400 do fiber.sleep(0.1) end;
 ---
 ...
+box.schema.func.drop('dml_push_and_dml')
+
 failed_catchers = {};
 ---
 ...
@@ -196,6 +210,12 @@ function push_null() box.session.push(box.NULL) end
 messages = {}
 ---
 ...
+box.schema.func.create('push_null')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'push_null')
+---
+...
 c:call('push_null', {}, {on_push = table.insert, on_push_ctx = messages})
 ---
 ...
@@ -203,6 +223,9 @@ messages
 ---
 - - null
 ...
+box.schema.func.drop('push_null')
+---
+...
 --
 -- Test binary pushes.
 --
@@ -272,6 +295,12 @@ t = setmetatable({100}, {__serialize = function() error('err in ser') end})
 function do_push() ok, err = box.session.push(t) end
 ---
 ...
+box.schema.func.create('do_push')
+---
+...
+box.schema.user.grant("guest", "execute", "function", "do_push")
+---
+...
 c:call('do_push', {}, {on_push = table.insert, on_push_ctx = messages})
 ---
 ...
@@ -284,6 +313,9 @@ messages
 ---
 - []
 ...
+box.schema.func.drop('do_push')
+---
+...
 --
 -- Test push from a non-call request.
 --
@@ -417,9 +449,18 @@ s:replace{1}
 ---
 - [1]
 ...
+box.schema.user.grant('guest', 'write', 'space', 'test')
+---
+...
 function do_push_and_duplicate() box.session.push(100) s:insert{1} end
 ---
 ...
+box.schema.func.create('do_push_and_duplicate')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'do_push_and_duplicate')
+---
+...
 future = c:call('do_push_and_duplicate', {}, {is_async = true})
 ---
 ...
@@ -447,12 +488,15 @@ keys
 - - 1
   - null
 ...
-s:drop()
+box.schema.func.drop('do_push_and_duplicate')
 ---
 ...
-c:close()
+box.schema.func.drop('do_pushes')
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+s:drop()
+---
+...
+c:close()
 ---
 ...
diff --git a/test/box/push.test.lua b/test/box/push.test.lua
index 36dc9eeb1..0d2bec3fe 100644
--- a/test/box/push.test.lua
+++ b/test/box/push.test.lua
@@ -23,7 +23,9 @@ end;
 test_run:cmd("setopt delimiter ''");
 
 netbox = require('net.box')
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
+
+box.schema.func.create('do_pushes')
+box.schema.user.grant('guest', 'execute', 'function', 'do_pushes')
 
 c = netbox.connect(box.cfg.listen)
 c:ping()
@@ -37,6 +39,7 @@ catchers = {}
 started = 0
 finished = 0
 s = box.schema.create_space('test', {format = {{'field1', 'integer'}}})
+box.schema.user.grant('guest', 'write', 'space', 'test')
 pk = s:create_index('pk')
 c:reload_schema()
 test_run:cmd("setopt delimiter ';'")
@@ -77,6 +80,9 @@ function dml_push_and_dml_f()
     table.insert(catchers, catcher)
     finished = finished + 1
 end;
+box.schema.func.create('dml_push_and_dml');
+box.schema.user.grant('guest', 'execute', 'function', 'dml_push_and_dml');
+
 -- At first check that a pushed message can be ignored in a binary
 -- protocol too.
 c:call('do_pushes', {300});
@@ -87,6 +93,8 @@ for i = 1, 200 do
 end;
 while finished ~= 400 do fiber.sleep(0.1) end;
 
+box.schema.func.drop('dml_push_and_dml')
+
 failed_catchers = {};
 
 for _, c in pairs(catchers) do
@@ -121,9 +129,11 @@ failed_catchers
 --
 function push_null() box.session.push(box.NULL) end
 messages = {}
+box.schema.func.create('push_null')
+box.schema.user.grant('guest', 'execute', 'function', 'push_null')
 c:call('push_null', {}, {on_push = table.insert, on_push_ctx = messages})
 messages
-
+box.schema.func.drop('push_null')
 --
 -- Test binary pushes.
 --
@@ -148,10 +158,12 @@ err = nil
 messages = {}
 t = setmetatable({100}, {__serialize = function() error('err in ser') end})
 function do_push() ok, err = box.session.push(t) end
+box.schema.func.create('do_push')
+box.schema.user.grant("guest", "execute", "function", "do_push")
 c:call('do_push', {}, {on_push = table.insert, on_push_ctx = messages})
 ok, err
 messages
-
+box.schema.func.drop('do_push')
 --
 -- Test push from a non-call request.
 --
@@ -187,7 +199,6 @@ function do_pushes()
     return true
 end;
 test_run:cmd("setopt delimiter ''");
-
 -- Can not combine callback and async mode.
 ok, err = pcall(c.call, c, 'do_pushes', {}, {is_async = true, on_push = function() end})
 ok
@@ -209,7 +220,11 @@ s = box.schema.create_space('test')
 pk = s:create_index('pk')
 s:replace{1}
 
+box.schema.user.grant('guest', 'write', 'space', 'test')
+
 function do_push_and_duplicate() box.session.push(100) s:insert{1} end
+box.schema.func.create('do_push_and_duplicate')
+box.schema.user.grant('guest', 'execute', 'function', 'do_push_and_duplicate')
 future = c:call('do_push_and_duplicate', {}, {is_async = true})
 future:wait_result(1000)
 messages = {}
@@ -218,7 +233,7 @@ for i, message in future:pairs() do table.insert(messages, message) table.insert
 messages
 keys
 
+box.schema.func.drop('do_push_and_duplicate')
+box.schema.func.drop('do_pushes')
 s:drop()
 c:close()
-
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
diff --git a/test/box/schema_reload.result b/test/box/schema_reload.result
index 31f215bc3..c927e4fcf 100644
--- a/test/box/schema_reload.result
+++ b/test/box/schema_reload.result
@@ -1,6 +1,3 @@
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 net_box = require('net.box')
 ---
 ...
@@ -17,6 +14,9 @@ s = box.schema.create_space('test')
 i = s:create_index('primary')
 ---
 ...
+box.schema.user.grant('guest', 'read', 'space', 'test')
+---
+...
 cn = net_box.connect(LISTEN.host, LISTEN.service)
 ---
 ...
@@ -35,6 +35,9 @@ s2 = box.schema.create_space('test2')
 i2 = s2:create_index('primary')
 ---
 ...
+box.schema.user.grant('guest', 'read', 'space', 'test2')
+---
+...
 ----------------------------------
 -- TEST #1 simple reload
 ----------------------------------
@@ -123,6 +126,9 @@ s:drop()
 s2:drop()
 ---
 ...
+cn:close()
+---
+...
 --------------------------------------------------------------------------------
 -- gh-1808: support schema_version in CALL, EVAL and PING
 --------------------------------------------------------------------------------
@@ -177,6 +183,12 @@ bump_schema_version()
 function somefunc() return true end
 ---
 ...
+box.schema.func.create('somefunc')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'somefunc')
+---
+...
 cn:call('somefunc')
 ---
 - true
@@ -209,6 +221,18 @@ cn.schema_version == schema_version + 1
 ---
 - true
 ...
+box.schema.func.drop('somefunc')
+---
+...
+cn:close()
+---
+...
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
+cn = net_box.connect(box.cfg.listen)
+---
+...
 -- eval
 schema_version = cn.schema_version
 ---
@@ -271,6 +295,6 @@ box.internal.schema_version() == schema_version + 1
 if box.space.bump_schema_version ~= nil then box.space.bump_schema_version:drop() end
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'universe')
 ---
 ...
diff --git a/test/box/schema_reload.test.lua b/test/box/schema_reload.test.lua
index 48ccb169c..cdfbfca75 100644
--- a/test/box/schema_reload.test.lua
+++ b/test/box/schema_reload.test.lua
@@ -1,4 +1,3 @@
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 net_box = require('net.box')
 fiber = require('fiber')
 LISTEN = require('uri').parse(box.cfg.listen)
@@ -6,6 +5,7 @@ LISTEN = require('uri').parse(box.cfg.listen)
 -- create first space
 s = box.schema.create_space('test')
 i = s:create_index('primary')
+box.schema.user.grant('guest', 'read', 'space', 'test')
 cn = net_box.connect(LISTEN.host, LISTEN.service)
 
 -- check that schema is correct
@@ -15,6 +15,7 @@ old_schema_version = cn.schema_version
 -- create one more space
 s2 = box.schema.create_space('test2')
 i2 = s2:create_index('primary')
+box.schema.user.grant('guest', 'read', 'space', 'test2')
 
 ----------------------------------
 -- TEST #1 simple reload
@@ -66,6 +67,7 @@ request_fiber:cancel()
 reload_fiber:cancel()
 s:drop()
 s2:drop()
+cn:close()
 
 --------------------------------------------------------------------------------
 -- gh-1808: support schema_version in CALL, EVAL and PING
@@ -95,6 +97,8 @@ cn.schema_version == schema_version + 1
 schema_version = cn.schema_version
 bump_schema_version()
 function somefunc() return true end
+box.schema.func.create('somefunc')
+box.schema.user.grant('guest', 'execute', 'function', 'somefunc')
 cn:call('somefunc')
 wait_new_schema()
 cn.schema_version == schema_version + 1
@@ -107,6 +111,12 @@ cn:call('somefunc')
 wait_new_schema()
 cn.schema_version == schema_version + 1
 
+box.schema.func.drop('somefunc')
+
+cn:close()
+box.schema.user.grant('guest', 'execute', 'universe')
+cn = net_box.connect(box.cfg.listen)
+
 -- eval
 schema_version = cn.schema_version
 bump_schema_version()
@@ -125,6 +135,7 @@ somefunc = nil
 
 cn:close()
 
+
 -- box.internal.schema_version()
 schema_version = box.internal.schema_version()
 schema_version > 0
@@ -132,5 +143,4 @@ bump_schema_version()
 box.internal.schema_version() == schema_version + 1
 
 if box.space.bump_schema_version ~= nil then box.space.bump_schema_version:drop() end
-
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+box.schema.user.revoke('guest', 'execute', 'universe')
diff --git a/test/box/sql.result b/test/box/sql.result
index 11a698850..a6c572f35 100644
--- a/test/box/sql.result
+++ b/test/box/sql.result
@@ -16,7 +16,10 @@ _ = box.schema.space.create('test1', { id = 555 })
 box.schema.user.create('test', { password = 'test' })
 ---
 ...
-box.schema.user.grant('test', 'execute,read,write', 'universe')
+box.schema.user.grant('test', 'read,write,alter', 'space', 'test1')
+---
+...
+box.schema.user.grant('test', 'read,write,alter', 'space', 'test')
 ---
 ...
 conn = net_box.connect('test:test@' .. box.cfg.listen)
@@ -228,10 +231,10 @@ net_box = require('net.box')
 box.schema.user.create('test', { password = 'test' })
 ---
 ...
-box.schema.user.grant('test', 'execute,read,write', 'universe')
+s = box.schema.space.create('tweedledum')
 ---
 ...
-s = box.schema.space.create('tweedledum')
+box.schema.user.grant('test', 'read,write,alter', 'space', 'tweedledum')
 ---
 ...
 index1 = s:create_index('primary', { type = 'tree', parts = { 1, 'string'} })
diff --git a/test/box/sql.test.lua b/test/box/sql.test.lua
index b1ba4168c..0523311ee 100644
--- a/test/box/sql.test.lua
+++ b/test/box/sql.test.lua
@@ -5,7 +5,8 @@ net_box = require('net.box')
 s = box.schema.space.create('test')
 _ = box.schema.space.create('test1', { id = 555 })
 box.schema.user.create('test', { password = 'test' })
-box.schema.user.grant('test', 'execute,read,write', 'universe')
+box.schema.user.grant('test', 'read,write,alter', 'space', 'test1')
+box.schema.user.grant('test', 'read,write,alter', 'space', 'test')
 
 conn = net_box.connect('test:test@' .. box.cfg.listen)
 space = conn.space.test
@@ -92,8 +93,8 @@ net_box = require('net.box')
 
 -- Prepare spaces
 box.schema.user.create('test', { password = 'test' })
-box.schema.user.grant('test', 'execute,read,write', 'universe')
 s = box.schema.space.create('tweedledum')
+box.schema.user.grant('test', 'read,write,alter', 'space', 'tweedledum')
 index1 = s:create_index('primary', { type = 'tree', parts = { 1, 'string'} })
 index2 = s:create_index('secondary', { type = 'tree', unique = false, parts = {2, 'string'}})
 function compare(a,b) return a[1] < b[1] end
diff --git a/test/box/stat_net.result b/test/box/stat_net.result
index fc1379920..b3e3db11f 100644
--- a/test/box/stat_net.result
+++ b/test/box/stat_net.result
@@ -19,7 +19,7 @@ box.stat.net.RECEIVED -- zero
 space = box.schema.space.create('tweedledum')
 ---
 ...
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest', 'read', 'space', 'tweedledum')
 ---
 ...
 index = space:create_index('primary', { type = 'hash' })
@@ -60,12 +60,9 @@ box.stat.net.RECEIVED.total
 ---
 - 0
 ...
-space:drop()
+space:drop() -- tweedledum
 ---
 ...
 cn:close()
 ---
 ...
-box.schema.user.revoke('guest','read,write,execute','universe')
----
-...
diff --git a/test/box/stat_net.test.lua b/test/box/stat_net.test.lua
index 9ddc55789..808bb71e7 100644
--- a/test/box/stat_net.test.lua
+++ b/test/box/stat_net.test.lua
@@ -7,7 +7,7 @@ box.stat.net.SENT -- zero
 box.stat.net.RECEIVED -- zero
 
 space = box.schema.space.create('tweedledum')
-box.schema.user.grant('guest','read,write,execute','universe')
+box.schema.user.grant('guest', 'read', 'space', 'tweedledum')
 index = space:create_index('primary', { type = 'hash' })
 remote = require 'net.box'
 
@@ -26,6 +26,5 @@ box.stat.reset()
 box.stat.net.SENT.total
 box.stat.net.RECEIVED.total
 
-space:drop()
+space:drop() -- tweedledum
 cn:close()
-box.schema.user.revoke('guest','read,write,execute','universe')
diff --git a/test/engine/params.result b/test/engine/params.result
index d5f4b3c82..debb40d40 100644
--- a/test/engine/params.result
+++ b/test/engine/params.result
@@ -8,9 +8,6 @@ inspector = test_run.new()
 engine = inspector:get_cfg('engine')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 s = box.schema.create_space('engine', {engine=engine})
 ---
 ...
@@ -30,6 +27,3 @@ box.space.engine:select{}
 box.space.engine:drop()
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
diff --git a/test/engine/params.test.lua b/test/engine/params.test.lua
index 3272835a1..a7cb499fd 100644
--- a/test/engine/params.test.lua
+++ b/test/engine/params.test.lua
@@ -3,7 +3,6 @@ test_run = require('test_run')
 inspector = test_run.new()
 engine = inspector:get_cfg('engine')
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 s = box.schema.create_space('engine', {engine=engine})
 i = s:create_index('primary')
 
@@ -14,4 +13,3 @@ box.space.engine:select{}
 
 -- cleanup
 box.space.engine:drop()
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
diff --git a/test/engine/replica_join.result b/test/engine/replica_join.result
index 39d857fef..7d6f50a19 100644
--- a/test/engine/replica_join.result
+++ b/test/engine/replica_join.result
@@ -10,9 +10,6 @@ engine = test_run:get_cfg('engine')
 index = test_run:get_cfg('index')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 box.schema.user.grant('guest', 'replication')
 ---
 ...
@@ -535,6 +532,3 @@ box.snapshot()
 box.schema.user.revoke('guest', 'replication')
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
diff --git a/test/engine/replica_join.test.lua b/test/engine/replica_join.test.lua
index 1792281e8..07320137b 100644
--- a/test/engine/replica_join.test.lua
+++ b/test/engine/replica_join.test.lua
@@ -2,7 +2,6 @@ env = require('test_run')
 test_run = env.new()
 engine = test_run:get_cfg('engine')
 index = test_run:get_cfg('index')
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 box.schema.user.grant('guest', 'replication')
 space = box.schema.space.create('test', { id = 99999, engine = engine })
 _ = space:create_index('primary', { type = index})
@@ -137,4 +136,3 @@ space:drop()
 box.snapshot()
 
 box.schema.user.revoke('guest', 'replication')
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
diff --git a/test/replication/autobootstrap.result b/test/replication/autobootstrap.result
index 04aeb4315..b660d0f2d 100644
--- a/test/replication/autobootstrap.result
+++ b/test/replication/autobootstrap.result
@@ -112,7 +112,16 @@ _ = test_run:cmd("switch autobootstrap1")
 u1 = box.schema.user.create('test_u')
 ---
 ...
-box.schema.user.grant('test_u', 'read,write,create', 'universe')
+box.schema.user.grant('test_u', 'create', 'space')
+---
+...
+box.schema.user.grant('test_u', 'read,write', 'space', '_space')
+---
+...
+box.schema.user.grant('test_u', 'write', 'space', '_schema')
+---
+...
+box.schema.user.grant('test_u', 'write', 'space', '_index')
 ---
 ...
 box.session.su('test_u')
@@ -131,6 +140,18 @@ box.space.test_u:select()
 ---
 - - [1, 2, 3, 4]
 ...
+box.schema.user.revoke('test_u', 'write', 'space', '_index')
+---
+...
+box.schema.user.revoke('test_u', 'write', 'space', '_schema')
+---
+...
+box.schema.user.revoke('test_u', 'read,write', 'space', '_space')
+---
+...
+box.schema.user.revoke('test_u', 'create', 'space')
+---
+...
 -- Synchronize
 vclock = test_run:get_vclock('autobootstrap1')
 ---
diff --git a/test/replication/autobootstrap.test.lua b/test/replication/autobootstrap.test.lua
index f1e2a9991..d952143b2 100644
--- a/test/replication/autobootstrap.test.lua
+++ b/test/replication/autobootstrap.test.lua
@@ -55,13 +55,21 @@ _ = test_run:cmd("switch default")
 
 _ = test_run:cmd("switch autobootstrap1")
 u1 = box.schema.user.create('test_u')
-box.schema.user.grant('test_u', 'read,write,create', 'universe')
+box.schema.user.grant('test_u', 'create', 'space')
+box.schema.user.grant('test_u', 'read,write', 'space', '_space')
+box.schema.user.grant('test_u', 'write', 'space', '_schema')
+box.schema.user.grant('test_u', 'write', 'space', '_index')
 box.session.su('test_u')
 _ = box.schema.space.create('test_u'):create_index('pk')
 box.session.su('admin')
 _ = box.space.test_u:replace({1, 2, 3, 4})
 box.space.test_u:select()
 
+box.schema.user.revoke('test_u', 'write', 'space', '_index')
+box.schema.user.revoke('test_u', 'write', 'space', '_schema')
+box.schema.user.revoke('test_u', 'read,write', 'space', '_space')
+box.schema.user.revoke('test_u', 'create', 'space')
+
 -- Synchronize
 vclock = test_run:get_vclock('autobootstrap1')
 _ = test_run:wait_vclock("autobootstrap2", vclock)
diff --git a/test/replication/catch.result b/test/replication/catch.result
index 91be32725..c8f8ca0a5 100644
--- a/test/replication/catch.result
+++ b/test/replication/catch.result
@@ -7,9 +7,6 @@ test_run = env.new()
 engine = test_run:get_cfg('engine')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 net_box = require('net.box')
 ---
 ...
@@ -139,6 +136,3 @@ box.space.test:drop()
 box.schema.user.revoke('guest', 'replication')
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
diff --git a/test/replication/catch.test.lua b/test/replication/catch.test.lua
index 2e2e97bc4..bba82e0e3 100644
--- a/test/replication/catch.test.lua
+++ b/test/replication/catch.test.lua
@@ -2,7 +2,6 @@ env = require('test_run')
 test_run = env.new()
 engine = test_run:get_cfg('engine')
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 
 net_box = require('net.box')
 errinj = box.error.injection
@@ -62,5 +61,4 @@ test_run:cmd("stop server replica")
 test_run:cmd("cleanup server replica")
 box.space.test:drop()
 box.schema.user.revoke('guest', 'replication')
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
 
diff --git a/test/replication/errinj.result b/test/replication/errinj.result
index ca8af2988..f2fa1df57 100644
--- a/test/replication/errinj.result
+++ b/test/replication/errinj.result
@@ -7,9 +7,6 @@ test_run = env.new()
 engine = test_run:get_cfg('engine')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 errinj = box.error.injection
 ---
 ...
diff --git a/test/replication/errinj.test.lua b/test/replication/errinj.test.lua
index 463d89a8f..eff9ef4b2 100644
--- a/test/replication/errinj.test.lua
+++ b/test/replication/errinj.test.lua
@@ -1,7 +1,6 @@
 env = require('test_run')
 test_run = env.new()
 engine = test_run:get_cfg('engine')
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 
 errinj = box.error.injection
 
diff --git a/test/replication/gc.result b/test/replication/gc.result
index e5c5cfccd..eeff6ac4b 100644
--- a/test/replication/gc.result
+++ b/test/replication/gc.result
@@ -27,9 +27,6 @@ function wait_gc(n) while #box.info.gc().checkpoints > n do fiber.sleep(0.01) en
 ---
 ...
 -- Grant permissions needed for replication.
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 box.schema.user.grant('guest', 'replication')
 ---
 ...
@@ -380,9 +377,6 @@ box.error.injection.set("ERRINJ_RELAY_REPORT_INTERVAL", 0)
 box.schema.user.revoke('guest', 'replication')
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
 box.cfg{checkpoint_count = default_checkpoint_count}
 ---
 ...
diff --git a/test/replication/gc.test.lua b/test/replication/gc.test.lua
index a465140c8..4034942cf 100644
--- a/test/replication/gc.test.lua
+++ b/test/replication/gc.test.lua
@@ -13,7 +13,6 @@ box.cfg{checkpoint_count = 1}
 function wait_gc(n) while #box.info.gc().checkpoints > n do fiber.sleep(0.01) end end
 
 -- Grant permissions needed for replication.
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 box.schema.user.grant('guest', 'replication')
 
 -- By default, relay thread reports status to tx once a second.
@@ -176,6 +175,5 @@ replica_set.drop_all(test_run)
 s:drop()
 box.error.injection.set("ERRINJ_RELAY_REPORT_INTERVAL", 0)
 box.schema.user.revoke('guest', 'replication')
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
 
 box.cfg{checkpoint_count = default_checkpoint_count}
diff --git a/test/replication/join_vclock.result b/test/replication/join_vclock.result
index 7c402dbf6..a9781073d 100644
--- a/test/replication/join_vclock.result
+++ b/test/replication/join_vclock.result
@@ -13,9 +13,6 @@ test_run = env.new()
 engine = test_run:get_cfg('engine')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 errinj = box.error.injection
 ---
 ...
@@ -87,9 +84,6 @@ replica_set.drop_all(test_run)
 box.space.test:drop()
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
 box.schema.user.revoke('guest', 'replication')
 ---
 ...
diff --git a/test/replication/join_vclock.test.lua b/test/replication/join_vclock.test.lua
index ac6eab75c..0b60dffc2 100644
--- a/test/replication/join_vclock.test.lua
+++ b/test/replication/join_vclock.test.lua
@@ -3,7 +3,6 @@ env = require('test_run')
 replica_set = require('fast_replica')
 test_run = env.new()
 engine = test_run:get_cfg('engine')
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 
 errinj = box.error.injection
 errinj.set("ERRINJ_RELAY_FINAL_SLEEP", true)
@@ -33,5 +32,4 @@ test_run:cmd("switch default")
 
 replica_set.drop_all(test_run)
 box.space.test:drop()
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
 box.schema.user.revoke('guest', 'replication')
diff --git a/test/replication/skip_conflict_row.result b/test/replication/skip_conflict_row.result
index bf794db5a..29963f56a 100644
--- a/test/replication/skip_conflict_row.result
+++ b/test/replication/skip_conflict_row.result
@@ -7,9 +7,6 @@ test_run = env.new()
 engine = test_run:get_cfg('engine')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 box.schema.user.grant('guest', 'replication')
 ---
 ...
@@ -100,6 +97,3 @@ box.space.test:drop()
 box.schema.user.revoke('guest', 'replication')
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
diff --git a/test/replication/skip_conflict_row.test.lua b/test/replication/skip_conflict_row.test.lua
index 695cce9db..5f7d6ead3 100644
--- a/test/replication/skip_conflict_row.test.lua
+++ b/test/replication/skip_conflict_row.test.lua
@@ -2,7 +2,6 @@ env = require('test_run')
 test_run = env.new()
 engine = test_run:get_cfg('engine')
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 box.schema.user.grant('guest', 'replication')
 
 space = box.schema.space.create('test', {engine = engine});
@@ -34,4 +33,3 @@ test_run:cmd("stop server replica")
 test_run:cmd("cleanup server replica")
 box.space.test:drop()
 box.schema.user.revoke('guest', 'replication')
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
diff --git a/test/vinyl/replica_quota.result b/test/vinyl/replica_quota.result
index 460cc1e61..50e397199 100644
--- a/test/vinyl/replica_quota.result
+++ b/test/vinyl/replica_quota.result
@@ -1,9 +1,6 @@
 test_run = require('test_run').new()
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 box.schema.user.grant('guest', 'replication')
 ---
 ...
@@ -94,6 +91,3 @@ s:drop()
 box.schema.user.revoke('guest', 'replication')
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
diff --git a/test/vinyl/replica_quota.test.lua b/test/vinyl/replica_quota.test.lua
index eade6f2f7..e04abbc22 100644
--- a/test/vinyl/replica_quota.test.lua
+++ b/test/vinyl/replica_quota.test.lua
@@ -1,6 +1,5 @@
 test_run = require('test_run').new()
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 box.schema.user.grant('guest', 'replication')
 
 s = box.schema.space.create('test', { engine = 'vinyl' })
@@ -49,4 +48,3 @@ _ = test_run:cmd("cleanup server replica")
 s:drop()
 
 box.schema.user.revoke('guest', 'replication')
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
diff --git a/test/wal_off/func_max.result b/test/wal_off/func_max.result
index 9211c4310..5a43821b2 100644
--- a/test/wal_off/func_max.result
+++ b/test/wal_off/func_max.result
@@ -36,36 +36,41 @@ function drop_limit_func()
 end;
 ---
 ...
-func_limit();
+test_run:cmd("setopt delimiter ''");
+---
+- true
+...
+func_limit()
 ---
 - error: 'A limit on the total number of functions has been reached: 32000'
 ...
-drop_limit_func();
+drop_limit_func()
 ---
 - error: Function 'func32000' does not exist
 ...
-box.schema.user.create('testuser');
+box.schema.user.create('testuser')
 ---
 ...
-box.schema.user.grant('testuser', 'read, write, execute,create', 'universe');
+box.schema.user.grant('testuser', 'read,write', 'space', '_func')
 ---
 ...
-session.su('testuser');
+box.schema.user.grant('testuser', 'create', 'function')
 ---
 ...
-func_limit();
+session.su('testuser')
+---
+...
+func_limit()
 ---
 - error: 'A limit on the total number of functions has been reached: 32000'
 ...
-drop_limit_func();
+drop_limit_func()
 ---
 - error: Function 'func32000' does not exist
 ...
 session.su('admin')
-box.schema.user.drop('testuser');
 ---
 ...
-test_run:cmd("setopt delimiter ''");
+box.schema.user.drop('testuser')
 ---
-- true
 ...
diff --git a/test/wal_off/func_max.test.lua b/test/wal_off/func_max.test.lua
index 00a095936..47fa6834b 100644
--- a/test/wal_off/func_max.test.lua
+++ b/test/wal_off/func_max.test.lua
@@ -21,13 +21,14 @@ function drop_limit_func()
         i = i + 1
     end
 end;
-func_limit();
-drop_limit_func();
-box.schema.user.create('testuser');
-box.schema.user.grant('testuser', 'read, write, execute,create', 'universe');
-session.su('testuser');
-func_limit();
-drop_limit_func();
-session.su('admin')
-box.schema.user.drop('testuser');
 test_run:cmd("setopt delimiter ''");
+func_limit()
+drop_limit_func()
+box.schema.user.create('testuser')
+box.schema.user.grant('testuser', 'read,write', 'space', '_func')
+box.schema.user.grant('testuser', 'create', 'function')
+session.su('testuser')
+func_limit()
+drop_limit_func()
+session.su('admin')
+box.schema.user.drop('testuser')
diff --git a/test/xlog/errinj.result b/test/xlog/errinj.result
index 262677f1d..6243ac701 100644
--- a/test/xlog/errinj.result
+++ b/test/xlog/errinj.result
@@ -50,15 +50,15 @@ test_run:cmd('restart server default with cleanup=1')
 errinj = box.error.injection
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 test = box.schema.create_space('test')
 ---
 ...
 _ = test:create_index('primary')
 ---
 ...
+box.schema.user.grant('guest', 'write', 'space', 'test')
+---
+...
 for i=1, box.cfg.rows_per_wal do test:insert{i, 'test'} end
 ---
 ...
@@ -85,6 +85,3 @@ test:drop()
 errinj = nil
 ---
 ...
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
diff --git a/test/xlog/errinj.test.lua b/test/xlog/errinj.test.lua
index 0ea15123f..7a5a29cb6 100644
--- a/test/xlog/errinj.test.lua
+++ b/test/xlog/errinj.test.lua
@@ -25,10 +25,11 @@ test_run:cmd('restart server default with cleanup=1')
 -- gh-881 iproto request with wal IO error
 errinj = box.error.injection
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 test = box.schema.create_space('test')
 _ = test:create_index('primary')
 
+box.schema.user.grant('guest', 'write', 'space', 'test')
+
 for i=1, box.cfg.rows_per_wal do test:insert{i, 'test'} end
 c = require('net.box').connect(box.cfg.listen)
 
@@ -40,4 +41,3 @@ errinj.set('ERRINJ_WAL_WRITE', false)
 -- Cleanup
 test:drop()
 errinj = nil
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
diff --git a/test/xlog/misc.result b/test/xlog/misc.result
index fd3362c4c..5df21aede 100644
--- a/test/xlog/misc.result
+++ b/test/xlog/misc.result
@@ -11,12 +11,12 @@ xlog = require('xlog')
 netbox = require('net.box')
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 --
 -- Check that xlogs doesn't contain IPROTO_SYNC
 --
+box.schema.user.grant('guest', 'write', 'space', '_schema')
+---
+...
 conn = netbox.connect(box.cfg.listen)
 ---
 ...
@@ -68,9 +68,6 @@ box.space._schema:delete('test')
 --
 -- Clean up
 --
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
----
-...
 netbox = nil
 ---
 ...
diff --git a/test/xlog/misc.test.lua b/test/xlog/misc.test.lua
index 6e67b86a9..44adfd942 100644
--- a/test/xlog/misc.test.lua
+++ b/test/xlog/misc.test.lua
@@ -5,12 +5,12 @@ fio = require('fio')
 xlog = require('xlog')
 netbox = require('net.box')
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
-
 --
 -- Check that xlogs doesn't contain IPROTO_SYNC
 --
 
+box.schema.user.grant('guest', 'write', 'space', '_schema')
+
 conn = netbox.connect(box.cfg.listen)
 -- insert some row using the binary protocol
 conn.space._schema:insert({'test'})
@@ -31,7 +31,6 @@ box.space._schema:delete('test')
 --
 -- Clean up
 --
-box.schema.user.revoke('guest', 'read,write,execute', 'universe')
 netbox = nil
 xlog = nil
 fio = nil
diff --git a/test/xlog/upgrade.result b/test/xlog/upgrade.result
index f02996bba..0bae10123 100644
--- a/test/xlog/upgrade.result
+++ b/test/xlog/upgrade.result
@@ -192,9 +192,9 @@ box.space._priv:select()
   - [1, 4, 'function', 3, 4]
   - [1, 4, 'role', 2, 4]
   - [1, 4, 'role', 5, 4]
-  - [1, 4, 'space', 513, 3]
+  - [1, 4, 'space', 513, 195]
   - [1, 4, 'universe', 0, 24]
-  - [1, 5, 'space', 512, 3]
+  - [1, 5, 'space', 512, 195]
   - [1, 31, 'universe', 0, 4294967295]
 ...
 box.space._vspace ~= nil
-- 
2.15.2 (Apple Git-101.1)

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [tarantool-patches] Re: [PATCH v2 3/4] Add single object privilege checks to access_check_ddl.
  2018-07-17 16:08   ` [tarantool-patches] [PATCH v2 3/4] Add single object privilege checks to access_check_ddl Serge Petrenko
@ 2018-07-26 20:37     ` Konstantin Osipov
  2018-07-30  8:37       ` Sergey Petrenko
  0 siblings, 1 reply; 11+ messages in thread
From: Konstantin Osipov @ 2018-07-26 20:37 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Serge Petrenko

* Serge Petrenko <sergepetrenko@tarantool.org> [18/07/17 21:31]:

This patch is generally LGTM, but it's based on entity access, so
I can't push it, since I requested some changes in entity access
patch. A few questions below.

> access_check_ddl() didn't check for single object privileges, e.g. user
> with alter access on a space couldn't create an index in this space. It
> would only succeed if it had alter on entire entity space.
> Fix this by adding single object privilege checks to access_check_ddl and
> adding access cache to struct user, to hold other users' privileges on it.
> 
> Also checking for single object privilege made it possible to grant
> every user alter privilege on itself, so that a user may change its own
> password (previously it was possible because of a hack). Removed the
> hack, and added grant alter to itself upon user creation.
> Modified tests accordingly, and added a couple of test cases.

What hack did you remove? The problem I have with self-granting
'alter' is that old deployments will break, since they don't have
a self-grant. Maybe we leave the hack in place for a while, begin
self-granting 'alter', and only remove the hack in 2.0? We
shouldn't forget to grant everyone an 'alter' on self in 2.0 upgrade script.
But to begin with, I can't find where this hack was in the code,
and where you removed it.

> Closes #3530
> ---
>  src/box/alter.cc         | 123 ++++++++++++++++++++++----------
>  src/box/lua/schema.lua   |   5 +-
>  src/box/user.cc          |  10 ++-
>  src/box/user.h           |   2 +
>  test/box/access.result   | 182 +++++++++++++++++++++++++++++++++++++++++++++++
>  test/box/access.test.lua |  53 ++++++++++++++
>  test/box/role.result     |   9 +++
>  test/box/sequence.result |   3 +
>  8 files changed, 347 insertions(+), 40 deletions(-)
> 
> diff --git a/src/box/alter.cc b/src/box/alter.cc
> index 6293dcc50..54a09664b 100644
> --- a/src/box/alter.cc
> +++ b/src/box/alter.cc
> @@ -62,7 +62,8 @@
>  /* {{{ Auxiliary functions and methods. */
>  
>  static void
> -access_check_ddl(const char *name, uint32_t owner_uid,
> +access_check_ddl(const char *name, uint32_t object_id,
> +		 uint32_t owner_uid,
>  		 enum schema_object_type type,
>  		 enum priv_type priv_type,
>  		 bool is_17_compat_mode)
> @@ -103,7 +104,48 @@ access_check_ddl(const char *name, uint32_t owner_uid,
>  	 */
>  	if (access == 0 || (is_owner && !(access & (PRIV_U | PRIV_C))))
>  		return; /* Access granted. */
> -
> +	/*
> +	 * You can't grant CREATE privilege to a non-existing object.
> +	 * USAGE can be granted only globally.
> +	 */
> +	if (!(access & (PRIV_U | PRIV_C))) {
> +		/* Check for privileges on a single object. */

Please avoid this deep nesting, use early returns from the
function for example, or create a separate function,
access_check_ddl_object()?

> +		switch (type) {
> +		case SC_SPACE:
> +		{
> +			struct space *space = space_by_id(object_id);
> +			if (space)
> +				access &= ~space->access[cr->auth_token].effective;
> +			break;
> +		}
> +		case SC_FUNCTION:
> +		{
> +			struct func *func = func_by_id(object_id);
> +			if (func)
> +				access &= ~func->access[cr->auth_token].effective;
> +			break;
> +		}
> +		case SC_USER:
> +		case SC_ROLE:
> +		{
> +			struct user *user_or_role = user_by_id(object_id);
> +			if (user_or_role)
> +				access &= ~user_or_role->access[cr->auth_token].effective;

> +			break;
> +		}
> +		case SC_SEQUENCE:
> +		{
> +			struct sequence *seq = sequence_by_id(object_id);
> +			if (seq)
> +				access &= ~seq->access[cr->auth_token].effective;
> +			break;
> +		}
> +		default:
> +			break;
> +		}

OK.

>  		 * check that it has read and write access.
>  		 */
> -		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
> -				 PRIV_R, false);
> -		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
> -				 PRIV_W, false);
> +		access_check_ddl(seq->def->name, seq->def->id, seq->def->uid,
> +				 SC_SEQUENCE, PRIV_R, false);
> +		access_check_ddl(seq->def->name, seq->def->id, seq->def->uid,
> +				 SC_SEQUENCE, PRIV_W, false);
>  	}
>  	/** Check we have alter access on space. */
> -	access_check_ddl(space->def->name, space->def->uid, SC_SPACE, PRIV_A,
> -			 false);
> +	access_check_ddl(space->def->name, space->def->id, space->def->uid,
> +			 SC_SPACE, PRIV_A, false);
>  
>  	struct trigger *on_commit =
>  		txn_alter_trigger_new(on_commit_dd_space_sequence, space);
> diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
> index 4b7a14411..a098e44fe 100644
> --- a/src/box/lua/schema.lua
> +++ b/src/box/lua/schema.lua
> @@ -1740,7 +1740,7 @@ local priv_object_combo = {
>      ["role"]     = bit.bor(box.priv.X, box.priv.U,
>                             box.priv.C, box.priv.D),
>      ["user"]	 = bit.bor(box.priv.C, box.priv.U,
> -                           box.priv.D),
> +                           box.priv.A, box.priv.D),
>  }
>      -- grant role 'public' to the user
>      box.schema.user.grant(uid, 'public')
> +    -- grant user 'alter' on itself, so it can
> +    -- change its password or username.
> +    box.schema.user.grant(uid, 'alter', 'user', uid)

-- 
Konstantin Osipov, Moscow, Russia, +7 903 626 22 32
http://tarantool.io - www.twitter.com/kostja_osipov

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [tarantool-patches] Re: [PATCH v2 3/4] Add single object privilege checks to access_check_ddl.
  2018-07-26 20:37     ` [tarantool-patches] " Konstantin Osipov
@ 2018-07-30  8:37       ` Sergey Petrenko
  0 siblings, 0 replies; 11+ messages in thread
From: Sergey Petrenko @ 2018-07-30  8:37 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tarantool-patches

Hi, I rebased this patch on top of the new one regarding #3524, and fixed
your comments. Please see 2 my comments and the new diff below.

> 26 июля 2018 г., в 23:37, Konstantin Osipov <kostja@tarantool.org> написал(а):
> 
> * Serge Petrenko <sergepetrenko@tarantool.org> [18/07/17 21:31]:
> 
> This patch is generally LGTM, but it's based on entity access, so
> I can't push it, since I requested some changes in entity access
> patch. A few questions below.
> 
>> access_check_ddl() didn't check for single object privileges, e.g. user
>> with alter access on a space couldn't create an index in this space. It
>> would only succeed if it had alter on entire entity space.
>> Fix this by adding single object privilege checks to access_check_ddl and
>> adding access cache to struct user, to hold other users' privileges on it.
>> 
>> Also checking for single object privilege made it possible to grant
>> every user alter privilege on itself, so that a user may change its own
>> password (previously it was possible because of a hack). Removed the
>> hack, and added grant alter to itself upon user creation.
>> Modified tests accordingly, and added a couple of test cases.
> 
> What hack did you remove? The problem I have with self-granting
> 'alter' is that old deployments will break, since they don't have
> a self-grant. Maybe we leave the hack in place for a while, begin
> self-granting 'alter', and only remove the hack in 2.0? We
> shouldn't forget to grant everyone an 'alter' on self in 2.0 upgrade script.
> But to begin with, I can't find where this hack was in the code,
> and where you removed it.

 Here’s the hack I was talking about: in function on_replace_dd_user in alter.cc
-		access_check_ddl(user->name, user->uid, SC_USER, PRIV_A,
-				 true);
We pass user as its own owner, which enables it to change its own password/username.

I leave the hack in place for now, and also add grant of alter on itself upon user creation:

+		access_check_ddl(user->name, user->uid, user->uid, SC_USER,
+				 PRIV_A, true);

schema.lua: function box.schema.user.create():
+    -- grant user 'alter' on itself, so it can
+    -- change its password or username.
+    box.schema.user.grant(uid, 'alter', 'user', uid)

> 
>> Closes #3530
>> ---
>> src/box/alter.cc         | 123 ++++++++++++++++++++++----------
>> src/box/lua/schema.lua   |   5 +-
>> src/box/user.cc          |  10 ++-
>> src/box/user.h           |   2 +
>> test/box/access.result   | 182 +++++++++++++++++++++++++++++++++++++++++++++++
>> test/box/access.test.lua |  53 ++++++++++++++
>> test/box/role.result     |   9 +++
>> test/box/sequence.result |   3 +
>> 8 files changed, 347 insertions(+), 40 deletions(-)
>> 
>> diff --git a/src/box/alter.cc b/src/box/alter.cc
>> index 6293dcc50..54a09664b 100644
>> --- a/src/box/alter.cc
>> +++ b/src/box/alter.cc
>> @@ -62,7 +62,8 @@
>> /* {{{ Auxiliary functions and methods. */
>> 
>> static void
>> -access_check_ddl(const char *name, uint32_t owner_uid,
>> +access_check_ddl(const char *name, uint32_t object_id,
>> +		 uint32_t owner_uid,
>> 		 enum schema_object_type type,
>> 		 enum priv_type priv_type,
>> 		 bool is_17_compat_mode)
>> @@ -103,7 +104,48 @@ access_check_ddl(const char *name, uint32_t owner_uid,
>> 	 */
>> 	if (access == 0 || (is_owner && !(access & (PRIV_U | PRIV_C))))
>> 		return; /* Access granted. */
>> -
>> +	/*
>> +	 * You can't grant CREATE privilege to a non-existing object.
>> +	 * USAGE can be granted only globally.
>> +	 */
>> +	if (!(access & (PRIV_U | PRIV_C))) {
>> +		/* Check for privileges on a single object. */
> 
> Please avoid this deep nesting, use early returns from the
> function for example, or create a separate function,
> access_check_ddl_object()?
Fixed. Added a jump to error case.

> 
>> +		switch (type) {
>> +		case SC_SPACE:
>> +		{
>> +			struct space *space = space_by_id(object_id);
>> +			if (space)
>> +				access &= ~space->access[cr->auth_token].effective;
>> +			break;
>> +		}
>> +		case SC_FUNCTION:
>> +		{
>> +			struct func *func = func_by_id(object_id);
>> +			if (func)
>> +				access &= ~func->access[cr->auth_token].effective;
>> +			break;
>> +		}
>> +		case SC_USER:
>> +		case SC_ROLE:
>> +		{
>> +			struct user *user_or_role = user_by_id(object_id);
>> +			if (user_or_role)
>> +				access &= ~user_or_role->access[cr->auth_token].effective;
> 
>> +			break;
>> +		}
>> +		case SC_SEQUENCE:
>> +		{
>> +			struct sequence *seq = sequence_by_id(object_id);
>> +			if (seq)
>> +				access &= ~seq->access[cr->auth_token].effective;
>> +			break;
>> +		}
>> +		default:
>> +			break;
>> +		}
> 
> OK.
> 
>> 		 * check that it has read and write access.
>> 		 */
>> -		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
>> -				 PRIV_R, false);
>> -		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
>> -				 PRIV_W, false);
>> +		access_check_ddl(seq->def->name, seq->def->id, seq->def->uid,
>> +				 SC_SEQUENCE, PRIV_R, false);
>> +		access_check_ddl(seq->def->name, seq->def->id, seq->def->uid,
>> +				 SC_SEQUENCE, PRIV_W, false);
>> 	}
>> 	/** Check we have alter access on space. */
>> -	access_check_ddl(space->def->name, space->def->uid, SC_SPACE, PRIV_A,
>> -			 false);
>> +	access_check_ddl(space->def->name, space->def->id, space->def->uid,
>> +			 SC_SPACE, PRIV_A, false);
>> 
>> 	struct trigger *on_commit =
>> 		txn_alter_trigger_new(on_commit_dd_space_sequence, space);
>> diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
>> index 4b7a14411..a098e44fe 100644
>> --- a/src/box/lua/schema.lua
>> +++ b/src/box/lua/schema.lua
>> @@ -1740,7 +1740,7 @@ local priv_object_combo = {
>>     ["role"]     = bit.bor(box.priv.X, box.priv.U,
>>                            box.priv.C, box.priv.D),
>>     ["user"]	 = bit.bor(box.priv.C, box.priv.U,
>> -                           box.priv.D),
>> +                           box.priv.A, box.priv.D),
>> }
>>     -- grant role 'public' to the user
>>     box.schema.user.grant(uid, 'public')
>> +    -- grant user 'alter' on itself, so it can
>> +    -- change its password or username.
>> +    box.schema.user.grant(uid, 'alter', 'user', uid)
> 
> -- 
> Konstantin Osipov, Moscow, Russia, +7 903 626 22 32
> http://tarantool.io - www.twitter.com/kostja_osipov
 
src/box/alter.cc         | 121 +++++++++++++++++++++---------
 src/box/lua/schema.lua   |   3 +
 src/box/user.cc          |   8 +-
 src/box/user.h           |   2 +
 test/box/access.result   | 186 +++++++++++++++++++++++++++++++++++++++++++++++
 test/box/access.test.lua |  57 +++++++++++++++
 test/box/role.result     |   9 +++
 test/box/sequence.result |   3 +
 8 files changed, 351 insertions(+), 38 deletions(-)

diff --git a/src/box/alter.cc b/src/box/alter.cc
index d4545f31c..35e4d0999 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -62,7 +62,8 @@
 /* {{{ Auxiliary functions and methods. */
 
 static void
-access_check_ddl(const char *name, uint32_t owner_uid,
+access_check_ddl(const char *name, uint32_t object_id,
+		 uint32_t owner_uid,
 		 enum schema_object_type type,
 		 enum priv_type priv_type,
 		 bool is_17_compat_mode)
@@ -103,9 +104,51 @@ access_check_ddl(const char *name, uint32_t owner_uid,
 	 */
 	if (access == 0 || (is_owner && !(access & (PRIV_U | PRIV_C))))
 		return; /* Access granted. */
+	/*
+	 * You can't grant CREATE privilege to a non-existing object.
+	 * USAGE can be granted only globally.
+	 */
+	if (access & (PRIV_U | PRIV_C))
+		goto error;
+	/* Check for privileges on a single object. */
+	switch (type) {
+	case SC_SPACE:
+	{
+		struct space *space = space_by_id(object_id);
+		if (space)
+			access &= ~space->access[cr->auth_token].effective;
+		break;
+	}
+	case SC_FUNCTION:
+	{
+		struct func *func = func_by_id(object_id);
+		if (func)
+			access &= ~func->access[cr->auth_token].effective;
+		break;
+	}
+	case SC_USER:
+	case SC_ROLE:
+	{
+		struct user *user_or_role = user_by_id(object_id);
+		if (user_or_role)
+			access &= ~user_or_role->access[cr->auth_token].effective;
+		break;
+	}
+	case SC_SEQUENCE:
+	{
+		struct sequence *seq = sequence_by_id(object_id);
+		if (seq)
+			access &= ~seq->access[cr->auth_token].effective;
+		break;
+	}
+	default:
+		break;
+	}
+	if (access == 0)
+	    return; /* Access granted. */
 
 	/* Create a meaningful error message. */
-	struct user *user = user_find_xc(cr->uid);
+error:	struct user *user = user_find_xc(cr->uid);
 	const char *object_name;
 	const char *pname;
 	if (access & PRIV_U) {
@@ -1590,7 +1633,8 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 		struct space_def *def =
 			space_def_new_from_tuple(new_tuple, ER_CREATE_SPACE,
 						 region);
-		access_check_ddl(def->name, def->uid, SC_SPACE, PRIV_C, true);
+		access_check_ddl(def->name, def->id, def->uid, SC_SPACE,
+				 PRIV_C, true);
 		auto def_guard =
 			make_scoped_guard([=] { space_def_delete(def); });
 		RLIST_HEAD(empty_list);
@@ -1623,8 +1667,8 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 			txn_alter_trigger_new(on_create_space_rollback, space);
 		txn_on_rollback(txn, on_rollback);
 	} else if (new_tuple == NULL) { /* DELETE */
-		access_check_ddl(old_space->def->name, old_space->def->uid,
-				 SC_SPACE, PRIV_D, true);
+		access_check_ddl(old_space->def->name, old_space->def->id,
+				 old_space->def->uid, SC_SPACE, PRIV_D, true);
 		/* Verify that the space is empty (has no indexes) */
 		if (old_space->index_count) {
 			tnt_raise(ClientError, ER_DROP_SPACE,
@@ -1669,7 +1713,8 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 		struct space_def *def =
 			space_def_new_from_tuple(new_tuple, ER_ALTER_SPACE,
 						 region);
-		access_check_ddl(def->name, def->uid, SC_SPACE, PRIV_A, true);
+		access_check_ddl(def->name, def->id, def->uid, SC_SPACE,
+				 PRIV_A, true);
 		auto def_guard =
 			make_scoped_guard([=] { space_def_delete(def); });
 		if (def->id != space_id(old_space))
@@ -1774,8 +1819,8 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
 	enum priv_type priv_type = new_tuple ? PRIV_C : PRIV_D;
 	if (old_tuple && new_tuple)
 		priv_type = PRIV_A;
-	access_check_ddl(old_space->def->name, old_space->def->uid, SC_SPACE,
-			 priv_type, true);
+	access_check_ddl(old_space->def->name, old_space->def->id,
+			 old_space->def->uid, SC_SPACE, priv_type, true);
 	struct index *old_index = space_index(old_space, iid);
 
 	/*
@@ -2170,7 +2215,8 @@ on_replace_dd_user(struct trigger * /* trigger */, void *event)
 	struct user *old_user = user_by_id(uid);
 	if (new_tuple != NULL && old_user == NULL) { /* INSERT */
 		struct user_def *user = user_def_new_from_tuple(new_tuple);
-		access_check_ddl(user->name, user->owner, user->type, PRIV_C, true);
+		access_check_ddl(user->name, user->uid, user->owner, user->type,
+				 PRIV_C, true);
 		auto def_guard = make_scoped_guard([=] { free(user); });
 		(void) user_cache_replace(user);
 		def_guard.is_active = false;
@@ -2178,7 +2224,8 @@ on_replace_dd_user(struct trigger * /* trigger */, void *event)
 			txn_alter_trigger_new(user_cache_remove_user, NULL);
 		txn_on_rollback(txn, on_rollback);
 	} else if (new_tuple == NULL) { /* DELETE */
-		access_check_ddl(old_user->def->name, old_user->def->owner,
+		access_check_ddl(old_user->def->name, old_user->def->uid,
+				 old_user->def->owner,
 				 old_user->def->type, PRIV_D, true);
 		/* Can't drop guest or super user */
 		if (uid <= (uint32_t) BOX_SYSTEM_USER_ID_MAX || uid == SUPER) {
@@ -2205,8 +2252,8 @@ on_replace_dd_user(struct trigger * /* trigger */, void *event)
 		 * correct.
 		 */
 		struct user_def *user = user_def_new_from_tuple(new_tuple);
-		access_check_ddl(user->name, user->uid, SC_USER, PRIV_A,
-				 true);
+		access_check_ddl(user->name, user->uid, user->uid, SC_USER,
+				 PRIV_A, true);
 		auto def_guard = make_scoped_guard([=] { free(user); });
 		struct trigger *on_commit =
 			txn_alter_trigger_new(user_cache_alter_user, NULL);
@@ -2308,7 +2355,8 @@ on_replace_dd_func(struct trigger * /* trigger */, void *event)
 	struct func *old_func = func_by_id(fid);
 	if (new_tuple != NULL && old_func == NULL) { /* INSERT */
 		struct func_def *def = func_def_new_from_tuple(new_tuple);
-		access_check_ddl(def->name, def->uid, SC_FUNCTION, PRIV_C, true);
+		access_check_ddl(def->name, def->fid, def->uid, SC_FUNCTION,
+				 PRIV_C, true);
 		auto def_guard = make_scoped_guard([=] { free(def); });
 		func_cache_replace(def);
 		def_guard.is_active = false;
@@ -2322,7 +2370,7 @@ on_replace_dd_func(struct trigger * /* trigger */, void *event)
 		 * Can only delete func if you're the one
 		 * who created it or a superuser.
 		 */
-		access_check_ddl(old_func->def->name, uid, SC_FUNCTION,
+		access_check_ddl(old_func->def->name, fid, uid, SC_FUNCTION,
 				 PRIV_D, true);
 		/* Can only delete func if it has no grants. */
 		if (schema_find_grants("function", old_func->def->fid)) {
@@ -2336,8 +2384,8 @@ on_replace_dd_func(struct trigger * /* trigger */, void *event)
 	} else {                                /* UPDATE, REPLACE */
 		struct func_def *def = func_def_new_from_tuple(new_tuple);
 		auto def_guard = make_scoped_guard([=] { free(def); });
-		access_check_ddl(def->name, def->uid, SC_FUNCTION, PRIV_A,
-				 true);
+		access_check_ddl(def->name, def->fid, def->uid,
+				 SC_FUNCTION, PRIV_A, true);
 		struct trigger *on_commit =
 			txn_alter_trigger_new(func_cache_replace_func, NULL);
 		txn_on_commit(txn, on_commit);
@@ -2493,8 +2541,9 @@ on_replace_dd_collation(struct trigger * /* trigger */, void *event)
 						    BOX_COLLATION_FIELD_ID);
 		struct coll_id *old_coll_id = coll_by_id(old_id);
 		assert(old_coll_id != NULL);
-		access_check_ddl(old_coll_id->name, old_coll_id->owner_id,
-				 SC_COLLATION, PRIV_D, false);
+		access_check_ddl(old_coll_id->name, old_id,
+				 old_coll_id->owner_id, SC_COLLATION, PRIV_D,
+				 false);
 		/*
 		 * Set on_commit/on_rollback triggers after
 		 * deletion from the cache to make trigger logic
@@ -2509,8 +2558,8 @@ on_replace_dd_collation(struct trigger * /* trigger */, void *event)
 		/* INSERT */
 		struct coll_id_def new_def;
 		coll_id_def_new_from_tuple(new_tuple, &new_def);
-		access_check_ddl(new_def.name, new_def.owner_id, SC_COLLATION,
-				 PRIV_C, false);
+		access_check_ddl(new_def.name, new_def.id, new_def.owner_id,
+				 SC_COLLATION, PRIV_C, false);
 		struct coll_id *new_coll_id = coll_id_new(&new_def);
 		if (new_coll_id == NULL)
 			diag_raise();
@@ -2569,8 +2618,8 @@ priv_def_check(struct priv_def *priv, enum priv_type priv_type)
 			  int2str(priv->grantee_id));
 	}
 	const char *name = schema_find_name(priv->object_type, priv->object_id);
-	access_check_ddl(name, grantor->def->uid, priv->object_type, priv_type,
-			 false);
+	access_check_ddl(name, priv->object_id, grantor->def->uid,
+			 priv->object_type, priv_type, false);
 	switch (priv->object_type) {
 	case SC_UNIVERSE:
 		if (grantor->def->uid != ADMIN) {
@@ -3075,8 +3124,8 @@ on_replace_dd_sequence(struct trigger * /* trigger */, void *event)
 		new_def = sequence_def_new_from_tuple(new_tuple,
 						      ER_CREATE_SEQUENCE);
 		assert(sequence_by_id(new_def->id) == NULL);
-		access_check_ddl(new_def->name, new_def->uid, SC_SEQUENCE,
-				 PRIV_C, false);
+		access_check_ddl(new_def->name, new_def->id, new_def->uid,
+				 SC_SEQUENCE, PRIV_C, false);
 		sequence_cache_replace(new_def);
 		alter->new_def = new_def;
 	} else if (old_tuple != NULL && new_tuple == NULL) {	/* DELETE */
@@ -3084,8 +3133,8 @@ on_replace_dd_sequence(struct trigger * /* trigger */, void *event)
 						 BOX_SEQUENCE_DATA_FIELD_ID);
 		struct sequence *seq = sequence_by_id(id);
 		assert(seq != NULL);
-		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
-				 PRIV_D, false);
+		access_check_ddl(seq->def->name, seq->def->id, seq->def->uid,
+				 SC_SEQUENCE, PRIV_D, false);
 		if (space_has_data(BOX_SEQUENCE_DATA_ID, 0, id))
 			tnt_raise(ClientError, ER_DROP_SEQUENCE,
 				  seq->def->name, "the sequence has data");
@@ -3101,8 +3150,8 @@ on_replace_dd_sequence(struct trigger * /* trigger */, void *event)
 						      ER_ALTER_SEQUENCE);
 		struct sequence *seq = sequence_by_id(new_def->id);
 		assert(seq != NULL);
-		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
-				 PRIV_A, false);
+		access_check_ddl(seq->def->name, seq->def->id, seq->def->uid,
+				 SC_SEQUENCE, PRIV_A, false);
 		alter->old_def = seq->def;
 		alter->new_def = new_def;
 	}
@@ -3182,21 +3231,21 @@ on_replace_dd_space_sequence(struct trigger * /* trigger */, void *event)
 
 	/* Check we have the correct access type on the sequence.  * */
 	if (is_generated || !stmt->new_tuple) {
-		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
-				 priv_type, false);
+		access_check_ddl(seq->def->name, seq->def->id, seq->def->uid,
+				 SC_SEQUENCE, priv_type, false);
 	} else {
 		/*
 		 * In case user wants to attach an existing sequence,
 		 * check that it has read and write access.
 		 */
-		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
-				 PRIV_R, false);
-		access_check_ddl(seq->def->name, seq->def->uid, SC_SEQUENCE,
-				 PRIV_W, false);
+		access_check_ddl(seq->def->name, seq->def->id, seq->def->uid,
+				 SC_SEQUENCE, PRIV_R, false);
+		access_check_ddl(seq->def->name, seq->def->id, seq->def->uid,
+				 SC_SEQUENCE, PRIV_W, false);
 	}
 	/** Check we have alter access on space. */
-	access_check_ddl(space->def->name, space->def->uid, SC_SPACE, PRIV_A,
-			 false);
+	access_check_ddl(space->def->name, space->def->id, space->def->uid,
+			 SC_SPACE, PRIV_A, false);
 
 	struct trigger *on_commit =
 		txn_alter_trigger_new(on_commit_dd_space_sequence, space);
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index 28f68cbda..350da210a 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -2103,6 +2103,9 @@ box.schema.user.create = function(name, opts)
     uid = _user:auto_increment{session.euid(), name, 'user', auth_mech_list}[1]
     -- grant role 'public' to the user
     box.schema.user.grant(uid, 'public')
+    -- grant user 'alter' on itself, so it can
+    -- change its password or username.
+    box.schema.user.grant(uid, 'alter', 'user', uid)
     -- we have to grant global privileges from setuid function, since
     -- only admin has the ownership over universe and we don't have
     -- grant option
diff --git a/src/box/user.cc b/src/box/user.cc
index b4fb65a59..83d07f7b3 100644
--- a/src/box/user.cc
+++ b/src/box/user.cc
@@ -248,12 +248,16 @@ access_find(struct priv_def *priv)
 	}
 	case SC_USER:
 	{
-		/* No grants on a single object user yet. */
+		struct user *user = user_by_id(priv->object_id);
+		if (user)
+			access = user->access;
 		break;
 	}
 	case SC_ROLE:
 	{
-		/* No grants on a single object role yet. */
+		struct user *role = user_by_id(priv->object_id);
+		if (role)
+			access = role->access;
 		break;
 	}
 	case SC_SEQUENCE:
diff --git a/src/box/user.h b/src/box/user.h
index 07c4dc504..069d9b77e 100644
--- a/src/box/user.h
+++ b/src/box/user.h
@@ -88,6 +88,8 @@ struct user
 	bool is_dirty;
 	/** Memory pool for privs */
 	struct region pool;
+	/** Cached runtime access imformation. */
+	struct access access[BOX_USER_MAX];
 };
 
 /** Find user by id. */
diff --git a/test/box/access.result b/test/box/access.result
index 7acd6fa43..819d8c471 100644
--- a/test/box/access.result
+++ b/test/box/access.result
@@ -136,6 +136,9 @@ box.schema.user.revoke('rich', 'read,write', 'universe')
 box.schema.user.revoke('rich', 'public')
 ---
 ...
+box.schema.user.revoke('rich', 'alter', 'user', 'rich')
+---
+...
 box.schema.user.disable("rich")
 ---
 ...
@@ -501,6 +504,7 @@ box.space._priv:select{id}
 ---
 - - [1, 32, 'role', 2, 4]
   - [1, 32, 'universe', 0, 27]
+  - [1, 32, 'user', 32, 128]
 ...
 box.schema.user.grant('user', 'read', 'universe')
 ---
@@ -510,6 +514,7 @@ box.space._priv:select{id}
 ---
 - - [1, 32, 'role', 2, 4]
   - [1, 32, 'universe', 0, 27]
+  - [1, 32, 'user', 32, 128]
 ...
 box.schema.user.revoke('user', 'write', 'universe')
 ---
@@ -518,6 +523,7 @@ box.space._priv:select{id}
 ---
 - - [1, 32, 'role', 2, 4]
   - [1, 32, 'universe', 0, 25]
+  - [1, 32, 'user', 32, 128]
 ...
 box.schema.user.revoke('user', 'read', 'universe')
 ---
@@ -526,6 +532,7 @@ box.space._priv:select{id}
 ---
 - - [1, 32, 'role', 2, 4]
   - [1, 32, 'universe', 0, 24]
+  - [1, 32, 'user', 32, 128]
 ...
 box.schema.user.grant('user', 'write', 'universe')
 ---
@@ -534,6 +541,7 @@ box.space._priv:select{id}
 ---
 - - [1, 32, 'role', 2, 4]
   - [1, 32, 'universe', 0, 26]
+  - [1, 32, 'user', 32, 128]
 ...
 box.schema.user.grant('user', 'read', 'universe')
 ---
@@ -542,6 +550,7 @@ box.space._priv:select{id}
 ---
 - - [1, 32, 'role', 2, 4]
   - [1, 32, 'universe', 0, 27]
+  - [1, 32, 'user', 32, 128]
 ...
 box.schema.user.drop('user')
 ---
@@ -965,6 +974,9 @@ box.schema.user.info('test_user')
   - - session,usage
     - universe
     - 
+  - - alter
+    - user
+    - test_user
 ...
 box.schema.role.info('test_role')
 ---
@@ -995,6 +1007,9 @@ box.schema.user.info('test_user')
   - - session,usage
     - universe
     - 
+  - - alter
+    - user
+    - test_user
 ...
 box.schema.role.info('test_role')
 ---
@@ -1860,3 +1875,174 @@ box.session.su('admin')
 box.schema.user.drop('tester')
 ---
 ...
+--
+-- test case for 3530: do not ignore single object privileges in
+-- access_check_ddl.
+--
+box.schema.user.create("test")
+---
+...
+_ = box.schema.space.create("space1")
+---
+...
+box.schema.user.grant("test", "read", "space", "space1")
+---
+...
+box.schema.user.grant("test", "write", "space", "_index")
+---
+...
+box.session.su("test")
+---
+...
+box.space.space1:create_index("pk")
+---
+- error: Create access to space 'space1' is denied for user 'test'
+...
+box.session.su("admin")
+---
+...
+box.space.space1.index[0] == nil
+---
+- true
+...
+-- fixme: cannot grant create on a single space
+-- this is because when checking for create
+-- access_check_ddl ignores space privileges,
+-- assuming that there is no space yet.
+box.schema.user.grant("test", "create", "space")
+---
+...
+box.session.su("test")
+---
+...
+_ = box.space.space1:create_index("pk")
+---
+...
+box.space.space1:insert{5}
+---
+- error: Write access to space 'space1' is denied for user 'test'
+...
+box.session.su("admin")
+---
+...
+box.space.space1.index[0] ~= nil
+---
+- true
+...
+box.space.space1:select{}
+---
+- []
+...
+box.schema.user.grant("test", "write", "space", "space1")
+---
+...
+box.session.su("test")
+---
+...
+box.space.space1:insert{5}
+---
+- [5]
+...
+box.session.su("admin")
+---
+...
+box.space.space1:select{}
+---
+- - [5]
+...
+box.schema.user.drop("test")
+---
+...
+box.space.space1:drop()
+---
+...
+--
+-- test that it is possible to grant privileges on a single user.
+box.schema.user.create("user1")
+---
+...
+box.schema.user.create("user2")
+---
+...
+box.schema.user.create("user3")
+---
+...
+box.schema.user.grant("user1", "write", "space", "_user")
+---
+...
+box.schema.user.grant("user1", "read", "space", "_user")
+---
+...
+box.space._user:select{}
+---
+- - [0, 1, 'guest', 'user', {'chap-sha1': 'vhvewKp0tNyweZQ+cFKAlsyphfg='}]
+  - [1, 1, 'admin', 'user', {}]
+  - [2, 1, 'public', 'role', {}]
+  - [3, 1, 'replication', 'role', {}]
+  - [31, 1, 'super', 'role', {}]
+  - [32, 1, 'user1', 'user', {}]
+  - [33, 1, 'user2', 'user', {}]
+  - [34, 1, 'user3', 'user', {}]
+...
+box.session.su("user1")
+---
+...
+-- can alter itself, but can't alter others without privileges.
+box.schema.user.passwd("user1", "abcd")
+---
+...
+box.schema.user.passwd("user2", "abcd")
+---
+- error: Alter access to user 'user2' is denied for user 'user1'
+...
+box.session.su("admin")
+---
+...
+box.space._user:select{}
+---
+- - [0, 1, 'guest', 'user', {'chap-sha1': 'vhvewKp0tNyweZQ+cFKAlsyphfg='}]
+  - [1, 1, 'admin', 'user', {}]
+  - [2, 1, 'public', 'role', {}]
+  - [3, 1, 'replication', 'role', {}]
+  - [31, 1, 'super', 'role', {}]
+  - [32, 1, 'user1', 'user', {'chap-sha1': 'oVTFJWXp5/lL/Aih/nAmJO2O/9o='}]
+  - [33, 1, 'user2', 'user', {}]
+  - [34, 1, 'user3', 'user', {}]
+...
+box.schema.user.grant("user1", "alter", "user", "user2")
+---
+...
+box.session.su("user1")
+---
+...
+box.schema.user.passwd("user2", "abcd")
+---
+...
+-- still fails
+box.schema.user.passwd("user3", "qewr")
+---
+- error: Alter access to user 'user3' is denied for user 'user1'
+...
+box.session.su("admin")
+---
+...
+box.space._user:select{}
+---
+- - [0, 1, 'guest', 'user', {'chap-sha1': 'vhvewKp0tNyweZQ+cFKAlsyphfg='}]
+  - [1, 1, 'admin', 'user', {}]
+  - [2, 1, 'public', 'role', {}]
+  - [3, 1, 'replication', 'role', {}]
+  - [31, 1, 'super', 'role', {}]
+  - [32, 1, 'user1', 'user', {'chap-sha1': 'oVTFJWXp5/lL/Aih/nAmJO2O/9o='}]
+  - [33, 1, 'user2', 'user', {'chap-sha1': 'oVTFJWXp5/lL/Aih/nAmJO2O/9o='}]
+  - [34, 1, 'user3', 'user', {}]
+...
+box.schema.user.drop("user1")
+---
+...
+box.schema.user.drop("user2")
+---
+...
+box.schema.user.drop("user3")
+---
+...
diff --git a/test/box/access.test.lua b/test/box/access.test.lua
index 9b7510e64..991ddf6ba 100644
--- a/test/box/access.test.lua
+++ b/test/box/access.test.lua
@@ -60,6 +60,7 @@ box.schema.func.drop('dummy')
 box.space['_user']:delete{uid}
 box.schema.user.revoke('rich', 'read,write', 'universe')
 box.schema.user.revoke('rich', 'public')
+box.schema.user.revoke('rich', 'alter', 'user', 'rich')
 box.schema.user.disable("rich")
 -- test double disable is a no op
 box.schema.user.disable("rich")
@@ -726,3 +727,59 @@ _ = box.schema.sequence.create('test_sequence')
 box.session.su('admin')
 box.schema.user.drop('tester')
 
+
+--
+-- test case for 3530: do not ignore single object privileges in
+-- access_check_ddl.
+--
+box.schema.user.create("test")
+_ = box.schema.space.create("space1")
+box.schema.user.grant("test", "read", "space", "space1")
+box.schema.user.grant("test", "write", "space", "_index")
+box.session.su("test")
+box.space.space1:create_index("pk")
+box.session.su("admin")
+box.space.space1.index[0] == nil
+-- fixme: cannot grant create on a single space
+-- this is because when checking for create
+-- access_check_ddl ignores space privileges,
+-- assuming that there is no space yet.
+box.schema.user.grant("test", "create", "space")
+box.session.su("test")
+_ = box.space.space1:create_index("pk")
+box.space.space1:insert{5}
+box.session.su("admin")
+box.space.space1.index[0] ~= nil
+box.space.space1:select{}
+box.schema.user.grant("test", "write", "space", "space1")
+box.session.su("test")
+box.space.space1:insert{5}
+box.session.su("admin")
+box.space.space1:select{}
+box.schema.user.drop("test")
+box.space.space1:drop()
+
+--
+-- test that it is possible to grant privileges on a single user.
+box.schema.user.create("user1")
+box.schema.user.create("user2")
+box.schema.user.create("user3")
+box.schema.user.grant("user1", "write", "space", "_user")
+box.schema.user.grant("user1", "read", "space", "_user")
+box.space._user:select{}
+box.session.su("user1")
+-- can alter itself, but can't alter others without privileges.
+box.schema.user.passwd("user1", "abcd")
+box.schema.user.passwd("user2", "abcd")
+box.session.su("admin")
+box.space._user:select{}
+box.schema.user.grant("user1", "alter", "user", "user2")
+box.session.su("user1")
+box.schema.user.passwd("user2", "abcd")
+-- still fails
+box.schema.user.passwd("user3", "qewr")
+box.session.su("admin")
+box.space._user:select{}
+box.schema.user.drop("user1")
+box.schema.user.drop("user2")
+box.schema.user.drop("user3")
diff --git a/test/box/role.result b/test/box/role.result
index 243c7bc6c..5666f7ef7 100644
--- a/test/box/role.result
+++ b/test/box/role.result
@@ -49,6 +49,9 @@ box.schema.user.info('tester')
   - - session,usage
     - universe
     - 
+  - - alter
+    - user
+    - tester
 ...
 box.schema.user.grant('tester', 'execute', 'role', 'iddqd')
 ---
@@ -64,6 +67,9 @@ box.schema.user.info('tester')
   - - session,usage
     - universe
     - 
+  - - alter
+    - user
+    - tester
 ...
 -- test granting user to a user
 box.schema.user.grant('tester', 'execute', 'role', 'tester')
@@ -956,6 +962,9 @@ box.schema.user.info('test_user')
   - - session,usage
     - universe
     - 
+  - - alter
+    - user
+    - test_user
 ...
 box.schema.role.info('test_user')
 ---
diff --git a/test/box/sequence.result b/test/box/sequence.result
index a2a1a60ea..b3907659f 100644
--- a/test/box/sequence.result
+++ b/test/box/sequence.result
@@ -1362,6 +1362,9 @@ box.schema.user.info()
   - - session,usage
     - universe
     - 
+  - - alter
+    - user
+    - user
 ...
 sq:set(100) -- ok
 ---
-- 
2.15.2 (Apple Git-101.1)

^ permalink raw reply	[flat|nested] 11+ messages in thread

end of thread, other threads:[~2018-07-30  8:37 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2018-07-17 15:47 [tarantool-patches] [PATCH 0/4] Fixes in access control and privileges Serge Petrenko
2018-07-17 15:47 ` [tarantool-patches] [PATCH 1/4] Make access_check_ddl check for entity privileges Serge Petrenko
2018-07-17 15:47 ` [tarantool-patches] [PATCH 2/4] Add entities user, role to access control Serge Petrenko
2018-07-17 15:47 ` [tarantool-patches] [PATCH 3/4] Add single object privilege checks to access_check_ddl Serge Petrenko
2018-07-17 15:47 ` [tarantool-patches] [PATCH 4/4] Add a privilege upgrade script and update tests Serge Petrenko
     [not found] ` <cover.1531843622.git.sergepetrenko@tarantool.org>
2018-07-17 16:08   ` [tarantool-patches] [PATCH v2 1/4] Make access_check_ddl check for entity privileges Serge Petrenko
2018-07-17 16:08   ` [tarantool-patches] [PATCH v2 2/4] Add entities user, role to access control Serge Petrenko
2018-07-17 16:08   ` [tarantool-patches] [PATCH v2 3/4] Add single object privilege checks to access_check_ddl Serge Petrenko
2018-07-26 20:37     ` [tarantool-patches] " Konstantin Osipov
2018-07-30  8:37       ` Sergey Petrenko
2018-07-17 16:08   ` [tarantool-patches] [PATCH v2 4/4] Add a privilege upgrade script and update tests Serge Petrenko

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox