Tarantool development patches archive
 help / color / mirror / Atom feed
* [tarantool-patches] [PATCH 0/4] Finish implementation of privileges.
@ 2018-08-20  8:10 Serge Petrenko
  2018-08-20  8:10 ` [tarantool-patches] [PATCH 1/4] Introduce separate entity object types for entity privileges Serge Petrenko
                   ` (3 more replies)
  0 siblings, 4 replies; 12+ messages in thread
From: Serge Petrenko @ 2018-08-20  8:10 UTC (permalink / raw)
  To: kostja; +Cc: tarantool-patches, Serge Petrenko

First patch introduces a separate schema_object_type for entity privileges
and adds '' (empty string) in object_id field to indicate grants on an entire
entity.
https://github.com/tarantool/tarantool/issues/3574

Second patch adds previously non-existent entities to access control: user
and role.
https://github.com/tarantool/tarantool/issues/3524

Third patch introduces single object access checks to access_checks_ddl.
Theese checks were previously ignored and in some cases granting privileges
on a single object had no effect.
https://github.com/tarantool/tarantool/issues/3530

Fourth patch adds an upgrade script for 1.10.2 to grant alter, create and
drop privileges to users with read and write privileges on objects.
https://github.com/tarantool/tarantool/issues/3539

Serge Petrenko (4):
  Introduce separate entity object types 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                            | 188 ++++++++++++++++----
 src/box/bootstrap.snap                      | Bin 1540 -> 1555 bytes
 src/box/lua/schema.lua                      |  90 ++++++----
 src/box/lua/upgrade.lua                     |  46 +++++
 src/box/schema.cc                           |  11 +-
 src/box/schema.h                            |  31 ++--
 src/box/schema_def.c                        |  21 +++
 src/box/schema_def.h                        |  18 +-
 src/box/user.cc                             |  53 ++++--
 src/box/user.h                              |   2 +
 test/box-py/bootstrap.result                |  14 +-
 test/box-tap/auth.test.lua                  |   5 -
 test/box-tap/session.test.lua               |  15 +-
 test/box/access.result                      | 216 +++++++++++++++++++++--
 test/box/access.test.lua                    |  76 ++++++--
 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                 |  14 +-
 test/box/access_misc.test.lua               |   4 +-
 test/box/alter.result                       |   8 +-
 test/box/call.result                        |   4 +-
 test/box/call.test.lua                      |   4 +-
 test/box/errinj.result                      |  39 +++-
 test/box/errinj.test.lua                    |  25 ++-
 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                    |   3 +
 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                    |  18 +-
 70 files changed, 1353 insertions(+), 378 deletions(-)

-- 
2.15.2 (Apple Git-101.1)

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

* [tarantool-patches] [PATCH 1/4] Introduce separate entity object types for entity privileges.
  2018-08-20  8:10 [tarantool-patches] [PATCH 0/4] Finish implementation of privileges Serge Petrenko
@ 2018-08-20  8:10 ` Serge Petrenko
  2018-08-22 10:28   ` Vladimir Davydov
  2018-08-22 12:37   ` Vladimir Davydov
  2018-08-20  8:10 ` [tarantool-patches] [PATCH 2/4] Add entities user, role to access control Serge Petrenko
                   ` (2 subsequent siblings)
  3 siblings, 2 replies; 12+ messages in thread
From: Serge Petrenko @ 2018-08-20  8:10 UTC (permalink / raw)
  To: kostja; +Cc: tarantool-patches, Serge Petrenko

When granting or revoking a privilege on an entire entity, id 0 was used
to indicate the fact that we don't grant a privilege on a single object,
but on a whole entity. This caused confusion, because for entity USER,
for example, id 0 is a valid object id (user 'guest' uses it).
Any non-zero id dedicated to this cause obviously may be confused as well.
Fix this by creating separate schema_object_types for entities:
SC_ENTITY_SPACE, SC_ENTITY_USER, etc.

Closes: #3574
Prerequisite: #3524
---
 src/box/alter.cc             |  27 +++++++++++++++++++-
 src/box/bootstrap.snap       | Bin 1540 -> 1556 bytes
 src/box/lua/schema.lua       |  58 ++++++++++++++++++++++++++-----------------
 src/box/lua/upgrade.lua      |  23 +++++++++++++++++
 src/box/schema.cc            |  11 ++++----
 src/box/schema.h             |  23 +++++++++--------
 src/box/schema_def.c         |  21 ++++++++++++++++
 src/box/schema_def.h         |  18 +++++++++++++-
 src/box/user.cc              |  27 +++++++++++---------
 test/box-py/bootstrap.result |  14 +++++------
 test/box/access.result       |   6 ++---
 test/box/access_misc.result  |   8 +++---
 test/box/alter.result        |   8 +++---
 test/xlog/upgrade.result     |  14 +++++------
 14 files changed, 180 insertions(+), 78 deletions(-)

diff --git a/src/box/alter.cc b/src/box/alter.cc
index 3007a131d..42136b7df 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -2537,10 +2537,35 @@ priv_def_create_from_tuple(struct priv_def *priv, struct tuple *tuple)
 {
 	priv->grantor_id = tuple_field_u32_xc(tuple, BOX_PRIV_FIELD_ID);
 	priv->grantee_id = tuple_field_u32_xc(tuple, BOX_PRIV_FIELD_UID);
+
 	const char *object_type =
 		tuple_field_cstr_xc(tuple, BOX_PRIV_FIELD_OBJECT_TYPE);
-	priv->object_id = tuple_field_u32_xc(tuple, BOX_PRIV_FIELD_OBJECT_ID);
 	priv->object_type = schema_object_type(object_type);
+
+	const char *data = tuple_field(tuple, BOX_PRIV_FIELD_OBJECT_ID);
+	if (data == NULL) {
+		tnt_raise(ClientError, ER_NO_SUCH_FIELD,
+		      BOX_PRIV_FIELD_OBJECT_ID + TUPLE_INDEX_BASE);
+	}
+	/*
+	 * When granting or revoking privileges on a whole entity
+	 * we pass empty string ('') to object_id to indicate
+	 * grant on every object of that entity.
+	 * So check for that first.
+	 */
+	switch (mp_typeof(*data)) {
+	case MP_STR:
+		if (mp_decode_strl(&data) == 0) {
+			/* Entity-wide privilege. */
+			priv->object_id = 0;
+			priv->object_type = schema_entity_type(priv->object_type);
+			break;
+		}
+		FALLTHROUGH;
+	default:
+		priv->object_id = tuple_field_u32_xc(tuple,
+						     BOX_PRIV_FIELD_OBJECT_ID);
+	}
 	if (priv->object_type == SC_UNKNOWN) {
 		tnt_raise(ClientError, ER_UNKNOWN_SCHEMA_OBJECT,
 			  object_type);
diff --git a/src/box/bootstrap.snap b/src/box/bootstrap.snap
index b610828c9c9ae9a22acdd8c150c16c6838b7a273..44992b050e9d73d69608fb92782d1e965f6cdc51 100644
GIT binary patch
delta 1551
zcmV+q2JrcW43rFz7=JM>GB!9ZXE|m$WMeloGhzx!ZgX^DZewLSAT%>EG&o@}VJ$c{
zGcheRV>UP~VPZ03EipATVq!NoIX7cCWeQe9Y;R+0Iv{&}3JTS_3%bn(nE=k-$Bkd5
z0000004TLD{QyvfEC41w*h<hAaRLBeJb=_J{}F0FAgJ*mVt>V;#?6&k&og$Aul<dv
zv3Dv(N-{sS_S_R$=If$P=IFJ-Qc5dSF3)wMv;i7#PyNg>UnEmX0mA^f0L=io5sT!g
zD{Fnf#vxPKYgy*n?1AMVIy{=p9gia^hC=L0fWLuz8rC_P&bxEnm$jbL+0jvRI_+sG
zru|Ih;m)d9Ab%fU2!MAhTHBbGce$H&nf~2*&yFmHLT-w8E2;yCj1eht_F<%$wYjrA
z--_qmirxZNlbP>7cRjpY(I5*1%V7?t>8y7vDuOB;TVebwm5d3mRG{g<l`_3ftd)rc
zn_;zHClt(+{hgbmxU+pKw$>&pI!j<g$w35)6B;Hdw}0vc|76#A%XVkpWtdu!!EHY`
zQ473V(OEa~Z9g}|)Sgs&h}w*uq0W{Rc(<Zvf8ImXW-LrCDFM%iVB<2)IG&t`=^X0p
z%Yh$fD8!Boc(<bFh|kp4F)S=%TMBr$q8k55lCM#${Y=DS+Bfbz&A~d;aQkK7b>5ZV
zdG3dP6Mx6^(8sb6!1;Xg8sDAa&WZ-$y>W_Zn~dpXc#Nf8o1_@R#?P@Ion`uUK3>En
z4|O&qz`GSKF%8FiNe275855I3zK&RX`$!^s><!J0t0shXfpC3jd1!TLaj@ww*s37t
z-HLiP9v};~La9)x)67Zcgesj%8+J-DBoj%4cYiCoL(1@BuzjT$lI`Wf$ik??2xi9d
ztc3#01eTBGMdTtHiCXRru~^$q5*v~bieRnCyA@ptRojk|H*sWb>UR9^o~tSri06BO
z0Q-*j>z9Q*4+(#G7_m1GyjxLQ$D%fO4}ENn1MgPUw1qv^wQ}ZNwi<Z1qOu?JnRn@f
zM}I7Goln@sxA7u>0uPF;){6CdrBo=>OXh`5;7UxHP%|pfgijbup=MGrPps{8(Quhz
zWw^YXTN9%N1NEZh;%hNtVMQxeEKqt?SWV_GYBSbz?d=0?f`WpY*c$IvG*qAOKeIM_
zQh2wbDYx3&2cp<tnmn~GC&WCKzkjS};(teS=fpg|%u;9X?K_pwnFhnDwIsY-(GRlN
ztgP{o#>Rx0;YE$dsj=v%(gx?x*@*;&XWUfcndq})KR0uozbQ*?y?v(<-mNH&3>7#_
z#&nJefg`BVSY%8M%m4s@04M+v21hAoRSyz?&^U^NFbZH021E!rh)xs$fZ#w_L4OfM
zEf@qq49vQeA))6t$8JsxhygJm2AP2;6jR1Ytzq>yuq)+j_YBuXSm^>oOc44oRR2+r
zfKn$8@9xdDJsnXgj8fvJX-OOEnOz2N82f7Vr6^tAcX*WFIPM{0Ntj!pLMS~2mv!oE
zRrM)J%oMN{r_N{V(s7_ah4$kjihs$6<OnC-Xy|vNdloebHTnit@EUCcD|n6OjV)em
zPC$*u2HNnREZ$rv)082si4@Jx$$z3_Ifq*d0kp^Vv;;5qa1a|JkDvz!Y7ZzgZLtTG
znKqel_G}Nk_kmTD8UY2Zw|;97AY4LmR2{?0eUDLb#Sq+kdJS#-4;^$U>3_%%=)mhn
z2W|IQc+vMG<^-d?>WOsJ?wF5DwFGHVZ;5{eV6!P9un202v<JSzI-4XFWp2pfJ=}&9
z|B3!R7$^i;Ur3=7lANu}q6eAjK|>w!Z8R4d19B91M{)$#!E>qp7jG6HO>MyhU^@4T
z^f(MLmIX$FanP|HqB(=3bAPgI29$n(?koqvkkh_+Jdt&9kemV%zz>2&MdX*2g^(0B
zb3fr-G#O-Ux`l`P)LF58aj(U8%fMRlqE2MOof#G*F;Vmq(Um!gfT;c#$##_N2vs0b
zGn`~?u8)RY;W>4D58f)EMzh}yokR<1H8AM^(7KO=Ygngs1QB1+=m<%o!R6Hut?jEW
B)aL*I

delta 1534
zcmV<a1p)e$41^4j7=JJ=G%zh^V>dT4F*z~{Np5p=VQyn(Iv_b=V_`LAF*YqTV`gS8
zG-hTvEnzrjFfCy?W;Qf4VP!dGGGq!?Lu_wjYdRo%F*+bOeF_TIx(m9^1&9F7A|rpb
zr2qf`001bpFZ}>e{VM<lJ=RLl7I6XqUp$Ch%_7lYJ_Lj_@PFoXP~+we(A76EO7~<&
z)F3;RA|;uhw(`4WgTRw>I&<{eU?~Ai$VOI~v629dx2JaIm@kx4N&&zCxB$xlBK}6v
zM#}nreKVfmYggvF>;bcooE<&pjzw|&ydgCd;B)Xi4C|Up=iRyONm;Mu=<J|49rmyj
z!~XSg?^aVP(0>n31mId~$~K1OP2OJJrGIzc!y|v*5Q}mxHFpG|K|%#jeQ^A;E^l>*
z3*Bp}NefgxX1+gm^KmUTgDMc1Jsb?P)oZC4!mL!?OgGcvU!7uNP_0u4V%4c@$$Tde
z<JJuBKo=+bJ1@oWHsew%rA$(CR4!bhCq$aiEJ3j}Cx2ttc)M<~yh-n@9Ru2aUZxgY
zOU+j=F>OCDy|bcHe0;hLW1g*=6kJQqvOo9n=`!@rT1v3PA=sEaGtUmrgF1V*+HwF$
z%o|ck2Ck)MDdN~U>lpNk)Rh9)Qd8p>$FU`TvVVQ}JNw3+pEy`&7|(v!cb#wKcU}Xc
zztr*E^M9!-1a3aZxQyRo-mQ)X;JxvQVS5a1E-Z#;*X8(mp!hlb(b=Wn_TxWHa?e&p
z0<NVdiD5jvlVdHR&M*6U`SOA@wvN<F<4PlXstt_|t42g+g<yeLeOP%|b(q;~x1wEl
zEj90~M>ms_NyVg8C=MtXRLX=hZO}2jlujiL*MCw|r<8%iFyl-urCG}bk_A!)lFN(Z
z*9rxd2`ZdQ%ZOz}8m(9>QmHeYBvm9q6rtA9wbTrW=uAh+mpZa8Z9M*WuhEnWw8OPP
z0Ken?`duE+M1Fc0sWuO;rRJ>TPnWxUKDEZdwbU$UUQcC(VtJFR2G>&aT`%TX-lUJ$
zzkkQk8h`!+Ak42Gb5O~I9X_GL4xhR(RGq@`<8YEmeGLBa%Ed{YaV#4wH7X6(wXrob
zRx(mCOfa|?Bo}qm;#z9jPz5?Ii>k-mKV61Bw$iw6MNm)>lUj2vHAVFK{#e#!kBV!l
ziDGG`ab1f2WXaFUg5t{}`TM8d6EBXt_J3uuRg$)9X&lOg&M*+4wU)S+njl=^i<EJZ
zX2ppwLyQ}LBXiMF<cgb*y<$jG_{C2oetkYX_VcpV`IoM<QW}RMaV<4zXt3a^V}T&3
z(OhIq3CsWhfB+}}AqGb&W<?JYfWSD4qc93!7zRWLI0_9I0RX{)ke~>l77Rj$ynhqj
zxeG%W18Ui|uoin^FOJH&6BE><l!R4e54%@5i^3tJ2=RjuhoRy}$qP!PWxP;CvBe2+
zB`AQ$m7GU4)NS@llAFhZ>1-;h#Us!zr2@yPWOP}$xk=>TS*Ej6Q8ZgBx&}sXh}Wym
z;Yvs=Xm6s&oTIkS5VXc28)t^2+kc4qR4r1#;*gdpU~^0s$pG8pm?G)gVGer7p5*j@
zrQSOro_cGm&fR}<@b8h^=iXW!%4reE=RqJFW<g443Z&4$8`^`Azzc12c=p(Su(w)T
z1)jmCmtK6UOn@l~=HxoFllhK1;>sqt`1BcS0HC@As3ba#z^Ko9Pi8avYk$Oa!!S#&
zSx3_!A8xyrpfT#3Y0zk_Hi}Y>p%$pI61F+5M3N&%2xKJAC;$CDD}<6?=$#5uoTZ5Z
zCNj}EgB|fKv=$L@ag>MEJ_76DnN*W1Z`O`MZ9ptwx|K1yxMV1V1(Ltx%(4y7obk4E
zW?=^8GIQ=kv4A0`wS7N1*>`Z!opukPx!_g@c{8|RJi<`!QS!z#<&+X#nnV8V6uo^%
zFL^|hfwSZ-+|LAZ&d5i?Zt>-$tMH_1UnT!Yb{@SussOv@WfJ50JyfLCQd9Bw_N-dh
k2=V>Mm|$S01}gnuSl5<E4okR>OyKJnJxL~`9Mur5?adb1g8%>k

diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index b9b8c9004..d347fd1e6 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -1801,16 +1801,16 @@ local function privilege_name(privilege)
 end
 
 local function object_resolve(object_type, object_name)
+    if object_name ~= nil and type(object_name) ~= 'string'
+            and type(object_name) ~= 'number' then
+        box.error(box.error.ILLEGAL_PARAMS, "wrong object name type")
+    end
     if object_type == 'universe' then
-        if object_name ~= nil and type(object_name) ~= 'string'
-                and type(object_name) ~= 'number' then
-            box.error(box.error.ILLEGAL_PARAMS, "wrong object name type")
-        end
         return 0
     end
     if object_type == 'space' then
-        if object_name == nil or object_name == 0 then
-            return 0
+        if object_name == '' then
+            return ''
         end
         local space = box.space[object_name]
         if  space == nil then
@@ -1819,8 +1819,8 @@ local function object_resolve(object_type, object_name)
         return space.id
     end
     if object_type == 'function' then
-        if object_name == nil or object_name == 0 then
-            return 0
+        if object_name == '' then
+            return ''
         end
         local _vfunc = box.space[box.schema.VFUNC_ID]
         local func
@@ -1836,8 +1836,8 @@ local function object_resolve(object_type, object_name)
         end
     end
     if object_type == 'sequence' then
-        if object_name == nil or object_name == 0 then
-            return 0
+        if object_name == '' then
+            return ''
         end
         local seq = sequence_resolve(object_name)
         if seq == nil then
@@ -1864,7 +1864,7 @@ local function object_resolve(object_type, object_name)
 end
 
 local function object_name(object_type, object_id)
-    if object_type == 'universe' then
+    if object_type == 'universe' or object_id == '' then
         return ""
     end
     local space
@@ -2072,12 +2072,18 @@ local function grant(uid, name, privilege, object_type,
                      object_name, options)
     -- From user point of view, role is the same thing
     -- as a privilege. Allow syntax grant(user, role).
-    if object_name == nil and object_type == nil then
-        -- sic: avoid recursion, to not bother with roles
-        -- named 'execute'
-        object_type = 'role'
-        object_name = privilege
-        privilege = 'execute'
+    if object_name == nil then
+        if object_type == nil then
+            -- sic: avoid recursion, to not bother with roles
+            -- named 'execute'
+            object_type = 'role'
+            object_name = privilege
+            privilege = 'execute'
+        else
+            -- Allow syntax grant(user, priv, entity)
+            -- for entity grants.
+            object_name = ''
+        end
     end
     local privilege_hex = privilege_check(privilege, object_type)
 
@@ -2117,10 +2123,16 @@ end
 local function revoke(uid, name, privilege, object_type, object_name, options)
     -- From user point of view, role is the same thing
     -- as a privilege. Allow syntax revoke(user, role).
-    if object_name == nil and object_type == nil then
-        object_type = 'role'
-        object_name = privilege
-        privilege = 'execute'
+    if object_name == nil then
+        if object_type == nil then
+            object_type = 'role'
+            object_name = privilege
+            privilege = 'execute'
+        else
+            -- Allow syntax revoke(user, privilege, entity)
+            -- to revoke entity privileges.
+            object_name = ''
+        end
     end
     local privilege_hex = privilege_check(privilege, object_type)
     options = options or {}
@@ -2192,8 +2204,8 @@ local function drop(uid, opts)
     local privs = _vpriv.index.primary:select{uid}
 
     for k, tuple in pairs(privs) do
-	-- we need an additional box.session.su() here, because of
-	-- unnecessary check for privilege PRIV_REVOKE in priv_def_check()
+        -- we need an additional box.session.su() here, because of
+        -- unnecessary check for privilege PRIV_REVOKE in priv_def_check()
         box.session.su("admin", revoke, uid, uid, tuple[5], tuple[3], tuple[4])
     end
     box.space[box.schema.USER_ID]:delete{uid}
diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua
index 0293f6ef8..091da2dc4 100644
--- a/src/box/lua/upgrade.lua
+++ b/src/box/lua/upgrade.lua
@@ -964,6 +964,28 @@ local function upgrade_to_1_10_0()
     create_vsequence_space()
 end
 
+--------------------------------------------------------------------------------
+--- Tarantool 1.10.2
+--------------------------------------------------------------------------------
+local function upgrade_space_priv_to_1_10_2()
+    local _priv = box.space._priv
+    local _vpriv = box.space._vpriv
+    local f = _priv:format()
+
+    f[4].type = 'scalar'
+    _priv:format(f)
+    f = _vpriv:format()
+    f[4].type = 'scalar'
+    _vpriv:format(f)
+    _priv.index.primary:alter{parts={2, 'unsigned', 3, 'string', 4, 'scalar'}}
+    _vpriv.index.primary:alter{parts={2, 'unsigned', 3, 'string', 4, 'scalar'}}
+    _priv.index.object:alter{parts={3, 'string', 4, 'scalar'}}
+    _vpriv.index.object:alter{parts={3, 'string', 4, 'scalar'}}
+end
+
+local function upgrade_to_1_10_2()
+    upgrade_space_priv_to_1_10_2()
+end
 
 local function get_version()
     local version = box.space._schema:get{'version'}
@@ -991,6 +1013,7 @@ local function upgrade(options)
         {version = mkversion(1, 7, 6), func = upgrade_to_1_7_6, auto = false},
         {version = mkversion(1, 7, 7), func = upgrade_to_1_7_7, auto = true},
         {version = mkversion(1, 10, 0), func = upgrade_to_1_10_0, auto = true},
+        {version = mkversion(1, 10, 2), func = upgrade_to_1_10_2, auto = true},
     }
 
     for _, handler in ipairs(handlers) do
diff --git a/src/box/schema.cc b/src/box/schema.cc
index 433f52c08..4502ca6dc 100644
--- a/src/box/schema.cc
+++ b/src/box/schema.cc
@@ -535,11 +535,14 @@ schema_find_name(enum schema_object_type type, uint32_t object_id)
 {
 	switch (type) {
 	case SC_UNIVERSE:
+	case SC_ENTITY_SPACE:
+	case SC_ENTITY_FUNCTION:
+	case SC_ENTITY_SEQUENCE:
+	case SC_ENTITY_ROLE:
+	case SC_ENTITY_USER:
 		return "";
 	case SC_SPACE:
 		{
-			if (object_id == 0)
-				return "SPACE";
 			struct space *space = space_by_id(object_id);
 			if (space == NULL)
 				break;
@@ -547,8 +550,6 @@ schema_find_name(enum schema_object_type type, uint32_t object_id)
 		}
 	case SC_FUNCTION:
 		{
-			if (object_id == 0)
-				return "FUNCTION";
 			struct func *func = func_by_id(object_id);
 			if (func == NULL)
 				break;
@@ -556,8 +557,6 @@ schema_find_name(enum schema_object_type type, uint32_t object_id)
 		}
 	case SC_SEQUENCE:
 		{
-			if (object_id == 0)
-				return "SEQUENCE";
 			struct sequence *seq = sequence_by_id(object_id);
 			if (seq == NULL)
 				break;
diff --git a/src/box/schema.h b/src/box/schema.h
index 0822262d0..f1735ff34 100644
--- a/src/box/schema.h
+++ b/src/box/schema.h
@@ -250,16 +250,19 @@ static inline
 struct access *
 entity_access_get(enum schema_object_type type)
 {
-       switch (type) {
-       case SC_SPACE:
-               return entity_access.space;
-       case SC_FUNCTION:
-               return entity_access.function;
-       case SC_SEQUENCE:
-               return entity_access.sequence;
-       default:
-               return NULL;
-       }
+	switch (type) {
+	case SC_SPACE:
+	case SC_ENTITY_SPACE:
+		return entity_access.space;
+	case SC_FUNCTION:
+	case SC_ENTITY_FUNCTION:
+		return entity_access.function;
+	case SC_SEQUENCE:
+	case SC_ENTITY_SEQUENCE:
+		return entity_access.sequence;
+	default:
+		return NULL;
+	}
 }
 
 #endif /* INCLUDES_TARANTOOL_BOX_SCHEMA_H */
diff --git a/src/box/schema_def.c b/src/box/schema_def.c
index 97c074ab2..5cc430d37 100644
--- a/src/box/schema_def.c
+++ b/src/box/schema_def.c
@@ -41,6 +41,27 @@ static const char *object_type_strs[] = {
 	/* [SC_COLLATION]       = */ "collation",
 };
 
+enum schema_object_type
+schema_entity_type(enum schema_object_type type)
+{
+	switch (type) {
+	case SC_SPACE:
+		return SC_ENTITY_SPACE;
+	case SC_FUNCTION:
+		return SC_ENTITY_FUNCTION;
+	case SC_USER:
+		return SC_ENTITY_USER;
+	case SC_ROLE:
+		return SC_ENTITY_ROLE;
+	case SC_SEQUENCE:
+		return SC_ENTITY_SEQUENCE;
+	case SC_COLLATION:
+		return SC_ENTITY_COLLATION;
+	default:
+		unreachable();
+	}
+}
+
 enum schema_object_type
 schema_object_type(const char *name)
 {
diff --git a/src/box/schema_def.h b/src/box/schema_def.h
index 2edb8d37f..e2826bd13 100644
--- a/src/box/schema_def.h
+++ b/src/box/schema_def.h
@@ -228,9 +228,25 @@ enum schema_object_type {
 	SC_ROLE = 5,
 	SC_SEQUENCE = 6,
 	SC_COLLATION = 7,
-	schema_object_type_MAX = 8
+	/*
+	 * All object types are supposed to be above this point,
+	 * all entity types - below.
+	 */
+	schema_object_type_MAX = 8,
+	SC_ENTITY_SPACE,
+	SC_ENTITY_FUNCTION,
+	SC_ENTITY_USER,
+	SC_ENTITY_ROLE,
+	SC_ENTITY_SEQUENCE,
+	SC_ENTITY_COLLATION
 };
 
+/**
+ * Given a object type, return an entity type it belongs to.
+ */
+enum schema_object_type
+schema_entity_type(enum schema_object_type type);
+
 enum schema_object_type
 schema_object_type(const char *name);
 
diff --git a/src/box/user.cc b/src/box/user.cc
index fbf06566a..eec785652 100644
--- a/src/box/user.cc
+++ b/src/box/user.cc
@@ -207,12 +207,23 @@ access_find(struct priv_def *priv)
 		access = universe.access;
 		break;
 	}
+	case SC_ENTITY_SPACE:
+	{
+		access = entity_access.space;
+		break;
+	}
+	case SC_ENTITY_FUNCTION:
+	{
+		access = entity_access.function;
+		break;
+	}
+	case SC_ENTITY_SEQUENCE:
+	{
+		access = entity_access.sequence;
+		break;
+	}
 	case SC_SPACE:
 	{
-		if (priv->object_id == 0) {
-			access = entity_access.space;
-			break;
-		}
 		struct space *space = space_by_id(priv->object_id);
 		if (space)
 			access = space->access;
@@ -220,10 +231,6 @@ access_find(struct priv_def *priv)
 	}
 	case SC_FUNCTION:
 	{
-		if (priv->object_id == 0) {
-			access = entity_access.function;
-			break;
-		}
 		struct func *func = func_by_id(priv->object_id);
 		if (func)
 			access = func->access;
@@ -231,10 +238,6 @@ access_find(struct priv_def *priv)
 	}
 	case SC_SEQUENCE:
 	{
-		if (priv->object_id == 0) {
-			access = entity_access.sequence;
-			break;
-		}
 		struct sequence *seq = sequence_by_id(priv->object_id);
 		if (seq)
 			access = seq->access;
diff --git a/test/box-py/bootstrap.result b/test/box-py/bootstrap.result
index 16c2027cf..cf8242de5 100644
--- a/test/box-py/bootstrap.result
+++ b/test/box-py/bootstrap.result
@@ -5,7 +5,7 @@ box.space._schema:select{}
 ---
 - - ['cluster', '<cluster uuid>']
   - ['max_id', 511]
-  - ['version', 1, 10, 0]
+  - ['version', 1, 10, 2]
 ...
 box.space._cluster:select{}
 ---
@@ -58,10 +58,10 @@ box.space._space:select{}
         'type': 'string'}, {'name': 'auth', 'type': 'map'}]]
   - [312, 1, '_priv', 'memtx', 0, {}, [{'name': 'grantor', 'type': 'unsigned'}, {
         'name': 'grantee', 'type': 'unsigned'}, {'name': 'object_type', 'type': 'string'},
-      {'name': 'object_id', 'type': 'unsigned'}, {'name': 'privilege', 'type': 'unsigned'}]]
+      {'name': 'object_id', 'type': 'scalar'}, {'name': 'privilege', 'type': 'unsigned'}]]
   - [313, 1, '_vpriv', 'sysview', 0, {}, [{'name': 'grantor', 'type': 'unsigned'},
       {'name': 'grantee', 'type': 'unsigned'}, {'name': 'object_type', 'type': 'string'},
-      {'name': 'object_id', 'type': 'unsigned'}, {'name': 'privilege', 'type': 'unsigned'}]]
+      {'name': 'object_id', 'type': 'scalar'}, {'name': 'privilege', 'type': 'unsigned'}]]
   - [320, 1, '_cluster', 'memtx', 0, {}, [{'name': 'id', 'type': 'unsigned'}, {'name': 'uuid',
         'type': 'string'}]]
   - [330, 1, '_truncate', 'memtx', 0, {}, [{'name': 'id', 'type': 'unsigned'}, {'name': 'count',
@@ -104,13 +104,13 @@ box.space._index:select{}
   - [305, 1, 'owner', 'tree', {'unique': false}, [[1, 'unsigned']]]
   - [305, 2, 'name', 'tree', {'unique': true}, [[2, 'string']]]
   - [312, 0, 'primary', 'tree', {'unique': true}, [[1, 'unsigned'], [2, 'string'],
-      [3, 'unsigned']]]
+      [3, 'scalar']]]
   - [312, 1, 'owner', 'tree', {'unique': false}, [[0, 'unsigned']]]
-  - [312, 2, 'object', 'tree', {'unique': false}, [[2, 'string'], [3, 'unsigned']]]
+  - [312, 2, 'object', 'tree', {'unique': false}, [[2, 'string'], [3, 'scalar']]]
   - [313, 0, 'primary', 'tree', {'unique': true}, [[1, 'unsigned'], [2, 'string'],
-      [3, 'unsigned']]]
+      [3, 'scalar']]]
   - [313, 1, 'owner', 'tree', {'unique': false}, [[0, 'unsigned']]]
-  - [313, 2, 'object', 'tree', {'unique': false}, [[2, 'string'], [3, 'unsigned']]]
+  - [313, 2, 'object', 'tree', {'unique': false}, [[2, 'string'], [3, 'scalar']]]
   - [320, 0, 'primary', 'tree', {'unique': true}, [[0, 'unsigned']]]
   - [320, 1, 'uuid', 'tree', {'unique': true}, [[1, 'string']]]
   - [330, 0, 'primary', 'tree', {'unique': true}, [[0, 'unsigned']]]
diff --git a/test/box/access.result b/test/box/access.result
index f4669a4a3..599500633 100644
--- a/test/box/access.result
+++ b/test/box/access.result
@@ -504,7 +504,7 @@ box.space._priv:select{id}
 ...
 box.schema.user.grant('user', 'read', 'universe')
 ---
-- error: User 'user' already has read access on universe 'nil'
+- error: User 'user' already has read access on universe ''
 ...
 box.space._priv:select{id}
 ---
@@ -690,7 +690,7 @@ box.schema.user.grant('guest', 'read,write,execute', 'universe')
 ...
 box.schema.user.grant('guest', 'read,write,execute', 'universe')
 ---
-- error: User 'guest' already has read,write,execute access on universe 'nil'
+- error: User 'guest' already has read,write,execute access on universe ''
 ...
 box.schema.user.grant('guest', 'read,write,execute', 'universe', '', { if_not_exists = true })
 ---
@@ -703,7 +703,7 @@ box.schema.user.revoke('guest', 'usage,session', 'universe')
 ...
 box.schema.user.revoke('guest', 'read,write,execute', 'universe')
 ---
-- error: User 'guest' does not have read,write,execute access on universe 'nil'
+- error: User 'guest' does not have read,write,execute access on universe ''
 ...
 box.schema.user.revoke('guest', 'read,write,execute', 'universe', '', { if_exists = true })
 ---
diff --git a/test/box/access_misc.result b/test/box/access_misc.result
index 2d87fa2d5..3061f1181 100644
--- a/test/box/access_misc.result
+++ b/test/box/access_misc.result
@@ -436,7 +436,7 @@ box.schema.user.revoke('testuser', 'usage,session', 'universe')
 ...
 box.schema.user.revoke('testuser', 'read, write, execute', 'universe')
 ---
-- error: User 'testuser' does not have read, write, execute access on universe 'nil'
+- error: User 'testuser' does not have read, write, execute access on universe ''
 ...
 box.schema.user.revoke('testuser', 'create', 'universe')
 ---
@@ -797,10 +797,10 @@ box.space._space:select()
         'type': 'string'}, {'name': 'auth', 'type': 'map'}]]
   - [312, 1, '_priv', 'memtx', 0, {}, [{'name': 'grantor', 'type': 'unsigned'}, {
         'name': 'grantee', 'type': 'unsigned'}, {'name': 'object_type', 'type': 'string'},
-      {'name': 'object_id', 'type': 'unsigned'}, {'name': 'privilege', 'type': 'unsigned'}]]
+      {'name': 'object_id', 'type': 'scalar'}, {'name': 'privilege', 'type': 'unsigned'}]]
   - [313, 1, '_vpriv', 'sysview', 0, {}, [{'name': 'grantor', 'type': 'unsigned'},
       {'name': 'grantee', 'type': 'unsigned'}, {'name': 'object_type', 'type': 'string'},
-      {'name': 'object_id', 'type': 'unsigned'}, {'name': 'privilege', 'type': 'unsigned'}]]
+      {'name': 'object_id', 'type': 'scalar'}, {'name': 'privilege', 'type': 'unsigned'}]]
   - [320, 1, '_cluster', 'memtx', 0, {}, [{'name': 'id', 'type': 'unsigned'}, {'name': 'uuid',
         'type': 'string'}]]
   - [330, 1, '_truncate', 'memtx', 0, {}, [{'name': 'id', 'type': 'unsigned'}, {'name': 'count',
@@ -853,7 +853,7 @@ box.schema.user.grant('tester', 'read', 'universe')
 -- error: the privilege is not granted
 box.schema.user.revoke('tester', 'create', 'universe')
 ---
-- error: User 'tester' does not have create access on universe 'nil'
+- error: User 'tester' does not have create access on universe ''
 ...
 -- no error: if_exists clause
 box.schema.user.revoke('tester', 'create', 'universe', nil, { if_exists = true })
diff --git a/test/box/alter.result b/test/box/alter.result
index eb7014d8b..36bdb5fd3 100644
--- a/test/box/alter.result
+++ b/test/box/alter.result
@@ -214,13 +214,13 @@ _index:select{}
   - [305, 1, 'owner', 'tree', {'unique': false}, [[1, 'unsigned']]]
   - [305, 2, 'name', 'tree', {'unique': true}, [[2, 'string']]]
   - [312, 0, 'primary', 'tree', {'unique': true}, [[1, 'unsigned'], [2, 'string'],
-      [3, 'unsigned']]]
+      [3, 'scalar']]]
   - [312, 1, 'owner', 'tree', {'unique': false}, [[0, 'unsigned']]]
-  - [312, 2, 'object', 'tree', {'unique': false}, [[2, 'string'], [3, 'unsigned']]]
+  - [312, 2, 'object', 'tree', {'unique': false}, [[2, 'string'], [3, 'scalar']]]
   - [313, 0, 'primary', 'tree', {'unique': true}, [[1, 'unsigned'], [2, 'string'],
-      [3, 'unsigned']]]
+      [3, 'scalar']]]
   - [313, 1, 'owner', 'tree', {'unique': false}, [[0, 'unsigned']]]
-  - [313, 2, 'object', 'tree', {'unique': false}, [[2, 'string'], [3, 'unsigned']]]
+  - [313, 2, 'object', 'tree', {'unique': false}, [[2, 'string'], [3, 'scalar']]]
   - [320, 0, 'primary', 'tree', {'unique': true}, [[0, 'unsigned']]]
   - [320, 1, 'uuid', 'tree', {'unique': true}, [[1, 'string']]]
   - [330, 0, 'primary', 'tree', {'unique': true}, [[0, 'unsigned']]]
diff --git a/test/xlog/upgrade.result b/test/xlog/upgrade.result
index f02996bba..76467baf1 100644
--- a/test/xlog/upgrade.result
+++ b/test/xlog/upgrade.result
@@ -36,7 +36,7 @@ box.space._schema:select()
 ---
 - - ['cluster', '<server_uuid>']
   - ['max_id', 513]
-  - ['version', 1, 10, 0]
+  - ['version', 1, 10, 2]
 ...
 box.space._space:select()
 ---
@@ -85,10 +85,10 @@ box.space._space:select()
         'type': 'string'}, {'name': 'auth', 'type': 'map'}]]
   - [312, 1, '_priv', 'memtx', 0, {}, [{'name': 'grantor', 'type': 'unsigned'}, {
         'name': 'grantee', 'type': 'unsigned'}, {'name': 'object_type', 'type': 'string'},
-      {'name': 'object_id', 'type': 'unsigned'}, {'name': 'privilege', 'type': 'unsigned'}]]
+      {'name': 'object_id', 'type': 'scalar'}, {'name': 'privilege', 'type': 'unsigned'}]]
   - [313, 1, '_vpriv', 'sysview', 0, {}, [{'name': 'grantor', 'type': 'unsigned'},
       {'name': 'grantee', 'type': 'unsigned'}, {'name': 'object_type', 'type': 'string'},
-      {'name': 'object_id', 'type': 'unsigned'}, {'name': 'privilege', 'type': 'unsigned'}]]
+      {'name': 'object_id', 'type': 'scalar'}, {'name': 'privilege', 'type': 'unsigned'}]]
   - [320, 1, '_cluster', 'memtx', 0, {}, [{'name': 'id', 'type': 'unsigned'}, {'name': 'uuid',
         'type': 'string'}]]
   - [330, 1, '_truncate', 'memtx', 0, {}, [{'name': 'id', 'type': 'unsigned'}, {'name': 'count',
@@ -134,13 +134,13 @@ box.space._index:select()
   - [305, 1, 'owner', 'tree', {'unique': false}, [[1, 'unsigned']]]
   - [305, 2, 'name', 'tree', {'unique': true}, [[2, 'string']]]
   - [312, 0, 'primary', 'tree', {'unique': true}, [[1, 'unsigned'], [2, 'string'],
-      [3, 'unsigned']]]
+      [3, 'scalar']]]
   - [312, 1, 'owner', 'tree', {'unique': false}, [[0, 'unsigned']]]
-  - [312, 2, 'object', 'tree', {'unique': false}, [[2, 'string'], [3, 'unsigned']]]
+  - [312, 2, 'object', 'tree', {'unique': false}, [[2, 'string'], [3, 'scalar']]]
   - [313, 0, 'primary', 'tree', {'unique': true}, [[1, 'unsigned'], [2, 'string'],
-      [3, 'unsigned']]]
+      [3, 'scalar']]]
   - [313, 1, 'owner', 'tree', {'unique': false}, [[0, 'unsigned']]]
-  - [313, 2, 'object', 'tree', {'unique': false}, [[2, 'string'], [3, 'unsigned']]]
+  - [313, 2, 'object', 'tree', {'unique': false}, [[2, 'string'], [3, 'scalar']]]
   - [320, 0, 'primary', 'tree', {'unique': true}, [[0, 'unsigned']]]
   - [320, 1, 'uuid', 'tree', {'unique': true}, [[1, 'string']]]
   - [330, 0, 'primary', 'tree', {'unique': true}, [[0, 'unsigned']]]
-- 
2.15.2 (Apple Git-101.1)

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

* [tarantool-patches] [PATCH 2/4] Add entities user, role to access control.
  2018-08-20  8:10 [tarantool-patches] [PATCH 0/4] Finish implementation of privileges Serge Petrenko
  2018-08-20  8:10 ` [tarantool-patches] [PATCH 1/4] Introduce separate entity object types for entity privileges Serge Petrenko
@ 2018-08-20  8:10 ` Serge Petrenko
  2018-08-22 10:37   ` Vladimir Davydov
  2018-08-22 12:53   ` Vladimir Davydov
  2018-08-20  8:10 ` [tarantool-patches] [PATCH 3/4] Add single object privilege checks to access_check_ddl Serge Petrenko
  2018-08-20  8:10 ` [tarantool-patches] [PATCH 4/4] Add a privilege upgrade script and update tests Serge Petrenko
  3 siblings, 2 replies; 12+ messages in thread
From: Serge Petrenko @ 2018-08-20  8:10 UTC (permalink / raw)
  To: kostja; +Cc: tarantool-patches, 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
Prerequisite #3530
---
 src/box/alter.cc            | 42 +++++++++++++++++++++++++++++++++++++++---
 src/box/lua/schema.lua      | 29 +++++++++++++++++++----------
 src/box/schema.h            |  8 ++++++++
 src/box/user.cc             | 22 +++++++++++++++++++++-
 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, 136 insertions(+), 39 deletions(-)

diff --git a/src/box/alter.cc b/src/box/alter.cc
index 42136b7df..436827a6d 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -2170,7 +2170,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;
@@ -2179,7 +2179,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,
@@ -2670,6 +2670,38 @@ priv_def_check(struct priv_def *priv, enum priv_type priv_type)
 		}
 		/* Not necessary to do during revoke, but who cares. */
 		role_check(grantee, role);
+		break;
+	}
+	case SC_USER:
+	{
+		struct user *user = NULL;
+		user = user_by_id(priv->object_id);
+		if (user == NULL || user->def->type != SC_USER) {
+			tnt_raise(ClientError, ER_NO_SUCH_USER,
+				  user ? user->def->name :
+				  int2str(priv->object_id));
+		}
+		if (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;
+	}
+	case SC_ENTITY_SPACE:
+	case SC_ENTITY_FUNCTION:
+	case SC_ENTITY_SEQUENCE:
+	case SC_ENTITY_ROLE:
+	case SC_ENTITY_USER:
+	{
+		/* Only amdin may grant privileges on an entire entity. */
+		if (grantor->def->uid != ADMIN) {
+			tnt_raise(AccessDeniedError, priv_name(priv_type),
+				  schema_object_name(priv->object_type), name,
+				  grantor->def->name);
+		}
 	}
 	default:
 		break;
@@ -2690,7 +2722,11 @@ 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)) {
 		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 d347fd1e6..bad35b680 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.A,
+                           box.priv.D),
 }
 
 --
@@ -1845,19 +1847,24 @@ 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
+        if object_name == '' then
+            return ''
+        end
         local _vuser = box.space[box.schema.VUSER_ID]
-        local role
+        local role_or_user
         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)
@@ -2111,7 +2118,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 ~= '' and
+               privilege == 'execute' then
                 box.error(box.error.ROLE_GRANTED, name, object_name)
             else
                 box.error(box.error.PRIV_GRANTED, name, privilege,
@@ -2145,7 +2153,8 @@ local function revoke(uid, name, privilege, object_type, object_name, options)
         if options.if_exists then
             return
         end
-        if object_type == 'role' then
+        if object_type == 'role' and object_name ~= '' and
+           privilege == 'execute' then
             box.error(box.error.ROLE_NOT_GRANTED, name, object_name)
         else
             box.error(box.error.PRIV_NOT_GRANTED, name, privilege,
diff --git a/src/box/schema.h b/src/box/schema.h
index f1735ff34..c343650de 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];
 };
 
@@ -257,6 +259,12 @@ entity_access_get(enum schema_object_type type)
 	case SC_FUNCTION:
 	case SC_ENTITY_FUNCTION:
 		return entity_access.function;
+	case SC_USER:
+	case SC_ENTITY_USER:
+		return entity_access.user;
+	case SC_ROLE:
+	case SC_ENTITY_ROLE:
+		return entity_access.role;
 	case SC_SEQUENCE:
 	case SC_ENTITY_SEQUENCE:
 		return entity_access.sequence;
diff --git a/src/box/user.cc b/src/box/user.cc
index eec785652..b4fb65a59 100644
--- a/src/box/user.cc
+++ b/src/box/user.cc
@@ -217,6 +217,16 @@ access_find(struct priv_def *priv)
 		access = entity_access.function;
 		break;
 	}
+	case SC_ENTITY_USER:
+	{
+		access = entity_access.user;
+		break;
+	}
+	case SC_ENTITY_ROLE:
+	{
+		access = entity_access.role;
+		break;
+	}
 	case SC_ENTITY_SEQUENCE:
 	{
 		access = entity_access.sequence;
@@ -236,6 +246,16 @@ access_find(struct priv_def *priv)
 			access = func->access;
 		break;
 	}
+	case SC_USER:
+	{
+		/* No grants on a single object user yet. */
+		break;
+	}
+	case SC_ROLE:
+	{
+		/* No grants on a single object role yet. */
+		break;
+	}
 	case SC_SEQUENCE:
 	{
 		struct sequence *seq = sequence_by_id(priv->object_id);
@@ -318,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 599500633..7ef2adac5 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 3061f1181..c1809d69a 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] 12+ messages in thread

* [tarantool-patches] [PATCH 3/4] Add single object privilege checks to access_check_ddl.
  2018-08-20  8:10 [tarantool-patches] [PATCH 0/4] Finish implementation of privileges Serge Petrenko
  2018-08-20  8:10 ` [tarantool-patches] [PATCH 1/4] Introduce separate entity object types for entity privileges Serge Petrenko
  2018-08-20  8:10 ` [tarantool-patches] [PATCH 2/4] Add entities user, role to access control Serge Petrenko
@ 2018-08-20  8:10 ` Serge Petrenko
  2018-08-22 11:58   ` Vladimir Davydov
  2018-08-20  8:10 ` [tarantool-patches] [PATCH 4/4] Add a privilege upgrade script and update tests Serge Petrenko
  3 siblings, 1 reply; 12+ messages in thread
From: Serge Petrenko @ 2018-08-20  8:10 UTC (permalink / raw)
  To: kostja; +Cc: tarantool-patches, 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). Added grant alter
to itself upon user creation.
Modified tests accordingly, and added a couple of test cases.

Closes #3530
Prerequisite #3539
---
 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 436827a6d..b58832612 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();
@@ -2594,8 +2643,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) {
@@ -3099,8 +3148,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 */
@@ -3108,8 +3157,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");
@@ -3125,8 +3174,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;
 	}
@@ -3206,21 +3255,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 bad35b680..1c254fe1d 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -2060,6 +2060,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 7ef2adac5..14f59230f 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] 12+ messages in thread

* [tarantool-patches] [PATCH 4/4] Add a privilege upgrade script and update tests.
  2018-08-20  8:10 [tarantool-patches] [PATCH 0/4] Finish implementation of privileges Serge Petrenko
                   ` (2 preceding siblings ...)
  2018-08-20  8:10 ` [tarantool-patches] [PATCH 3/4] Add single object privilege checks to access_check_ddl Serge Petrenko
@ 2018-08-20  8:10 ` Serge Petrenko
  2018-08-22 12:48   ` Vladimir Davydov
  3 siblings, 1 reply; 12+ messages in thread
From: Serge Petrenko @ 2018-08-20  8:10 UTC (permalink / raw)
  To: kostja; +Cc: tarantool-patches, Serge Petrenko

This patch adds a privilege upgrade script, which runs on upgrade to
1.10.2 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/bootstrap.snap                      | Bin 1556 -> 1555 bytes
 src/box/lua/upgrade.lua                     |  23 +++
 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                      |  39 +++-
 test/box/errinj.test.lua                    |  25 ++-
 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 +-
 57 files changed, 689 insertions(+), 226 deletions(-)

diff --git a/src/box/bootstrap.snap b/src/box/bootstrap.snap
index 44992b050e9d73d69608fb92782d1e965f6cdc51..7f214e019d316a58689d6c0e6f40d27a812b3a5e 100644
GIT binary patch
delta 1430
zcmV;H1!?+}43i9y8-F=5EoU__HZWo|VP<0rNp5p=VQyn(Iv_PQVK-$pWMeHdW@ctB
zG&yB9En#C}W-U1|I5}oHVqr5dIbsS{Lu_wjYdRo%eF_TIx(m9^1(*QN^Y_tyr2qf`
z001bpFZ}>eeJlVbz1K<57I6arUp&AU-y+dqH(0h4*l!hp|9@?Au~44{l+5aDRdrXC
zTZ%99zp_%;Z$4xEWR9K})E4?c8<3Sf)i+QjABdtA)&V7D3d8`p0L=ik5N9#8jj`V~
z*AdT8bJzBq{Q33}9v?35kHb*R9+A2dK;xik80Iw>-~D@=9%H}8)1!m#e3*uv7^Yv@
zd$+1mg??}%0Dsp~GuAO|_wn}nF0T9cKRb5zh}e{Csi{NA3=%46RexgMoV-;ZZslG}
zO<SOHar>^zn~!U$IaGzd?d@QgtzJt_+>)6|eIM#mrD{$!p9-I7WLh()Po)A+tr;4b
zTtm~rK<WB>GR!v_r&1|xqQVox$q<hqQJ&Z^NwJhCT7Qe5<L<hZyN}*kR}5xddos1)
zT57&NiDzAV(mN|k#Yg5OSbDZ<QgAIb$*;YS%t_EYYbk*bhoIx}+%%sZ{qgPD>dOHj
zrAMTa3|vc1GQ{O6`}p&W)RqF*Qj=pHief9wG5yLopTFbp&mGJ+jMlYFf4*;{{=No;
zYchxS9)D0}A)upo>0|y@dbc_nfP2R$hIJ9Nxo`-cpOa$tzEE#4h;J9ywjVm<(R;Qk
z5^yawM+~F+KZ*fedom_wM{FUfb;gxO{L~zq9M+A9$_l{((S7JVbRA?iyXxp_buBfo
ztjDS*MUzscW>7LH4ycp~W!lhViYc8+8m^^gPk$)`he5`fUP`i;3nUAq3M819$FCL&
zEE8BbmY0#sh%{=kR>V?eJxZ!bf+#|*qid<TBBHXMC7R5!IceLWb^kT0QiXiD7YLvJ
zxNELmp5H~hpdLqR&Vy^IDf>8cvVRY#-Z;3HnxxG8sjQW9_pxhmEj3*_+~w{g5Dszn
zHh&=RXMLk31_iASC4*ZX*sW0A3B(znXmmGo0zr_&DSs+mgH<XRC{@O>YPis-Ggvpr
z-pJ@;q+pm{a4twK>WIZk6*}#T%EkRNC&3?EXWR;BXlSTOy}6c}BC37Y<>us%ifgHv
zVriXms}x<2q(7?*ioC;7_n>-D=uqsnyiLOvIohh7aVQgf!#;f0TH;!2f^dPaG3G-X
zoD(?%3>#V}*5ac^935T0$|NcL@~08MGSH5$Jz4u+ldiMU8HXZqEj4Xste{m9<U1yl
z;{$kq<&yA*v9DHNiqhqMhe!F1;~pZGgt-MOgwj)RS*N~MRiBc?OaWVQ>U_2?9S8bT
zXg@BZn0!c%aMF#2emA;jQIk-kZ(s$l(KfJx*J$3@;>G3!)M#v=4e!a~&2=(O8N!-K
z(fpkJCpwmMxU~>Kdu&fj@L~@Ku_5vZdT^kB_JA_e7JERMX_E<O&-So;A6PZ15m3;2
z>$e60!X*?()iJEx_ZSsd48gsp*U-lQ&_Rcij{JZQyl!;Rc8`S@eLrGOFxso0NJs6C
z`M6X|kQVip_*Vcnn-T(xpq5B`;47@NNm5bfh8*6*Z8-6t=--2ZLXh=^6gnZv*}5!$
zdXSkOG}IB_MstxdAV+a`Bu8K!JeTT!@n-SS)D}ztrgN`IkHZjSSzsg>2OZlXnlm^$
zC(C9)>G$W(au5tT?Tg0~SqBHnDIfv-AXrpHepy)vNntbh6W&FWLB^(Ac(_lU726m0
zT5PurtR*k%L?+ysVKEXDMK2LunUe@Ti0XflY)8qCPz542!%5cW`e@h{o>Ry7;H?5`
kH2dAqNwknw1B3n#t@}v0hILv;5b+g_o+KJv4%HB??ay<dv;Y7A

delta 1431
zcmV;I1!(${43rFz8-F%9EoV7qIb>ruGc#feNp5p=VQyn(Iv_MNF*G<~Fkvk?H8U|S
zG-Eb6En#9ZVl6Q>G-6^mHaRzAIAsb}Lu_wjYdRo%eF_TIx(m9^1(^WO-p7q!r2qf`
z001bpFZ}>eg)9IjJ=jXn7I6XqUp#=+EdLQ|J|L*^AY#R!#(&L~S<f?ekgxrXsIhk{
zMM^S1wf5W-S?24aPUh&f!BR>qR4&hTqO<`TZ%_TqF<&H8N&&+Fxd6=oxe<%xs4HuI
zzs4a`*lStl+U$YlAUZsn%pH#-DTYGqN`SwCdm7d`na;a&-IukV)7jBcb2{y5DW?5Q
z<l)Y$SRfx?2!DWgD_Yx_mUp?Eb(#L%dC!h4hC*(NcPpv`h>Q^_aQ0!On6<gHJl~4v
z-HP4<R+E|UKX*O6ThSm31j}I#rs=GAD=LC299v=hE0v51uT-Gvzm+n*POO!Q1)E{D
zUMCdHll`5WqqwtuDz?@pDmqJGM9Dz}iW3?pDYxnb|9@oHc*}NY-es6tkil&~H&F|`
zThUoJ@ohgh!_=NsdWhPLouST_6nM9yW`Ev8)MhM9Ehz!dhhXC}%{ZQ%hv^*Z?8|{4
zXDGyu40yMq=7`VK)-fzBVp|G$x1t*VNRqEnto=;HV%j(EJk7y6({TG`-*w)V-+Atb
zeiO&@(0|9W5Wx9-@*3Zr;m(Q%;JtB*X`77cWO$6FU7MsB!p6_BADw0Tbv|CiB@cBr
zB*42BEinzpdr1cSxfv6aL%xn!d;3Ttdh89&jjJYvb%AhwXnAOLXmPOVF4(Fd=-rBX
zHXa}gwL+;-sng6!=7cJpN*i`cF(ea7gLf;sLx0NfVX%Fr7n1Ge!pOp?!U$%@@vMac
z%LJB><wfKo8i`u&4Y63;P7)iE5Q<=}$h#F?302#Uk~eW=ZR&RX@1CnF7KrD2fdKoC
z_v@F1JP!$fco?xa54>AZTgRd{cMpARjRWsi)U<^?*0plxUA7u{x1zEi^O<+)gGVfK
zoqtc*#kcVye*zDRtk#P4dZkn-(@W-sP2fsQnNTw-(1cGIOrd5{Fi))QbJ1{_VP&|y
zn_Cm31q1b><l<{FVqrxqRxD6@Rai~tE^0H@bM5T|ZGwV=n%EleRy0(f??1CPds29}
zqA9o9+Xtf9Uz$9%E+@o1mcM_jXW~b4=TF2uzRXf*@9jI4(3u9qskJ1$ThR})*sQGa
zk;cY^nBhf@$EmUCr_u)J&)JCtg=gGU;+g2PV?Q@@oxdqdZM}V`5#FsRjSLkyOU87L
z36tRicz<06Zy5V(^`$6X-gkJE-#G3eVo8`=ph74;1($W|YgP3rNz4?m6{pT;>(X(c
zKZW+=B8tg}<OnC-Xy|vNdloebHTnit@EUCcD|n6OjV)emPC$*u2HNnREZ$rv)082s
zi4@Jx$$z3_Ifq*d0kp^Vv;;5qa1a|JkDvz!YJU$XGi|X4l$kb}aQ18uyZ3=rlNtd9
zt+#$_5FlJaaa0|{%6*Sfam5hadwLCR{0|*;DCx)#=)mhn2W|IQc+vMG<^-d?>WOsJ
z?wF5DwFGHVZ;5{eV6!P9un202v<JSzI-4XFWp2pfJ=}&9|B3!R7$^i;Ur3=7lANu}
zqJIaO=|Mvs@oh9083S?@cSmvr*1>bB{ugf+A5Cq+1YkP%iu5=PF_r~Jf^pEX9ilmd
zqjR!s29$n(?koqvkkh_+Jdt&9kemV%zz>2&MdX*2g^(0Bb3fr-G#O-Ux`l`P)LF58
zaj(U8%fMRlqE2MOof#G*F;Vmq(Um!gfIO)F7s+;%><CpLQZt-nZLW`oUEw)(d=K6#
lphmOb4V^>_X*DqD|IoURglkx*bp#P#(dbE{!R6Hut?gN!sVe{g

diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua
index 091da2dc4..d8d03288c 100644
--- a/src/box/lua/upgrade.lua
+++ b/src/box/lua/upgrade.lua
@@ -983,8 +983,31 @@ local function upgrade_space_priv_to_1_10_2()
     _vpriv.index.object:alter{parts={3, 'string', 4, 'scalar'}}
 end
 
+local function upgrade_users_to_1_10_2()
+    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)
+                    -- for universal grants
+                    if priv[3] == 'universe' 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_2()
     upgrade_space_priv_to_1_10_2()
+    upgrade_users_to_1_10_2()
 end
 
 local function get_version()
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 14f59230f..2fd6386a0 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 991ddf6ba..74edf9979 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 c1809d69a..af1a82c20 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 35234898d..7783dc131 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 c7e4ce20b..3b7347bc9 100644
--- a/test/box/errinj.result
+++ b/test/box/errinj.result
@@ -786,7 +786,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)
@@ -818,7 +818,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()
@@ -1071,15 +1071,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)
 ---
 ...
@@ -1110,12 +1110,24 @@ 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', 'create', 'space')
+---
+...
+box.schema.user.grant('guest', 'write', 'space', '_index')
+---
+...
 s:replace{1, 1}
 ---
 - [1, 1]
@@ -1166,6 +1178,12 @@ cn:close()
 s:drop()
 ---
 ...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
+box.schema.user.revoke('guest', 'create', 'space')
+---
+...
 --
 -- If message memory pool is used up, stop the connection, until
 -- the pool has free memory.
@@ -1191,6 +1209,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
@@ -1252,7 +1276,10 @@ 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')
+---
+...
+box.schema.func.drop('long_poll_f')
 ---
 ...
 --
diff --git a/test/box/errinj.test.lua b/test/box/errinj.test.lua
index a3ea659aa..1c1149e7d 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,15 @@ 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', 'create', 'space')
+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,7 +394,8 @@ while ok == nil do fiber.sleep(0.01) end
 ok, err
 cn:close()
 s:drop()
-
+box.schema.user.revoke('guest', 'execute', 'universe')
+box.schema.user.revoke('guest', 'create', 'space')
 --
 -- If message memory pool is used up, stop the connection, until
 -- the pool has free memory.
@@ -403,6 +410,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 +439,8 @@ 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')
+box.schema.func.drop('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 37ac54658..bd91f63c2 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' })
@@ -1119,6 +1170,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
@@ -1142,6 +1202,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)
 ---
@@ -1186,13 +1249,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
@@ -1212,6 +1278,9 @@ test_run:cmd("setopt delimiter ''");
 gh594()
 ---
 ...
+box.schema.func.drop('dostring')
+---
+...
 -- #636: Reload schema on demand
 sp = box.schema.space.create('test_old')
 ---
@@ -1223,6 +1292,9 @@ sp:insert{1, 2, 3}
 ---
 - [1, 2, 3]
 ...
+box.schema.user.grant('guest', 'read', 'space', 'test_old')
+---
+...
 con = remote.new(box.cfg.listen)
 ---
 ...
@@ -1248,6 +1320,9 @@ sp:insert{2, 3, 4}
 ---
 - [2, 3, 4]
 ...
+box.schema.user.grant('guest', 'read', 'space', 'test')
+---
+...
 con.space.test == nil
 ---
 - true
@@ -1278,6 +1353,9 @@ file_log:seek(0, 'SEEK_END') ~= 0
 ---
 - true
 ...
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
 test_run:cmd("setopt delimiter ';'")
 ---
 - true
@@ -1299,6 +1377,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
@@ -1314,6 +1395,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')
 ---
 ...
@@ -1339,6 +1423,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)
 ---
 ...
@@ -1395,6 +1482,9 @@ box.space.test:drop()
 function echo(...) return ... end
 ---
 ...
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
 c = net.connect(box.cfg.listen)
 ---
 ...
@@ -1434,6 +1524,9 @@ c:eval('return echo(...)', 42)
 c:close()
 ---
 ...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
 --
 -- gh-2195 export pure msgpack from net.box
 --
@@ -1443,6 +1536,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)
 ---
 ...
@@ -1678,6 +1777,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}
@@ -1695,6 +1797,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 ';'")
 ---
@@ -1747,7 +1852,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)
@@ -1836,7 +1941,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)
@@ -1915,7 +2020,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)
@@ -1928,6 +2033,9 @@ c:call("box.session.type")
 c:close()
 ---
 ...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
 --
 -- On_connect/disconnect triggers.
 --
@@ -2007,6 +2115,9 @@ space:drop()
 space = box.schema.space.create('test')
 ---
 ...
+box.schema.user.grant('guest', 'read', 'space', 'test')
+---
+...
 c = net.connect(box.cfg.listen)
 ---
 ...
@@ -2040,16 +2151,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)
@@ -2166,6 +2289,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.
@@ -2185,6 +2317,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)
 ---
 ...
@@ -2233,6 +2371,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.
@@ -2343,7 +2484,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')
@@ -2382,6 +2523,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')
 ---
 ...
@@ -2436,7 +2580,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
@@ -2452,7 +2596,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')
@@ -2476,6 +2620,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')
 ---
 ...
@@ -2514,6 +2667,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
 ---
 ...
@@ -2621,6 +2780,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})
 ---
 ...
@@ -2641,6 +2809,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.
@@ -2748,6 +2925,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})
 ---
 ...
@@ -2854,6 +3040,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})
 ---
 ...
@@ -2861,6 +3056,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})
 ---
 ...
@@ -3029,12 +3233,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})
 ---
 ...
@@ -3071,6 +3293,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.
@@ -3122,6 +3347,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 56b656cc5..08fe60fd5 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
@@ -427,6 +447,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')
 
@@ -436,7 +459,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)
@@ -455,10 +478,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)
@@ -468,12 +492,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{}
@@ -483,6 +510,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{}
@@ -495,6 +524,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(
@@ -506,6 +536,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
@@ -518,6 +549,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})
@@ -528,6 +560,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
@@ -545,6 +578,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})
@@ -556,6 +590,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
@@ -563,6 +598,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()
 
@@ -649,6 +686,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()
@@ -658,6 +696,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
@@ -699,7 +739,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)
@@ -745,7 +785,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)
 
@@ -785,10 +825,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.
@@ -818,6 +860,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
@@ -829,15 +872,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
@@ -894,6 +940,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.
@@ -903,6 +952,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
@@ -917,6 +968,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
@@ -971,7 +1023,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)
@@ -984,6 +1036,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}
@@ -1008,7 +1061,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
 
@@ -1016,7 +1069,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)
@@ -1024,6 +1077,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()
@@ -1038,6 +1094,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')
@@ -1075,13 +1133,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.
@@ -1120,6 +1184,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
@@ -1153,8 +1220,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}
@@ -1219,10 +1292,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})
@@ -1235,6 +1315,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
@@ -1263,4 +1344,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 f2de06f90..ac5a540c2 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 91badc1f1..7b770a5ea 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 752d5f317..3b1397eb5 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 0f72e89e2..aebba819f 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 457f910e9..8cc3242f7 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 3fc432010..ce6add8d4 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 37375f45e..e1e96a0c4 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 3f9db26ce..83d0de293 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')
 ---
 ...
@@ -463,9 +460,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 96f11f8d4..eed76850c 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.
@@ -212,6 +211,5 @@ box.cfg{replication = {}}
 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 76467baf1..b4cf18c71 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] 12+ messages in thread

* Re: [tarantool-patches] [PATCH 1/4] Introduce separate entity object types for entity privileges.
  2018-08-20  8:10 ` [tarantool-patches] [PATCH 1/4] Introduce separate entity object types for entity privileges Serge Petrenko
@ 2018-08-22 10:28   ` Vladimir Davydov
  2018-08-22 12:37   ` Vladimir Davydov
  1 sibling, 0 replies; 12+ messages in thread
From: Vladimir Davydov @ 2018-08-22 10:28 UTC (permalink / raw)
  To: Serge Petrenko; +Cc: kostja, tarantool-patches

On Mon, Aug 20, 2018 at 11:10:05AM +0300, Serge Petrenko wrote:
> diff --git a/src/box/alter.cc b/src/box/alter.cc
> index 3007a131d..42136b7df 100644
> --- a/src/box/alter.cc
> +++ b/src/box/alter.cc
> @@ -2537,10 +2537,35 @@ priv_def_create_from_tuple(struct priv_def *priv, struct tuple *tuple)
>  {
>  	priv->grantor_id = tuple_field_u32_xc(tuple, BOX_PRIV_FIELD_ID);
>  	priv->grantee_id = tuple_field_u32_xc(tuple, BOX_PRIV_FIELD_UID);
> +
>  	const char *object_type =
>  		tuple_field_cstr_xc(tuple, BOX_PRIV_FIELD_OBJECT_TYPE);
> -	priv->object_id = tuple_field_u32_xc(tuple, BOX_PRIV_FIELD_OBJECT_ID);
>  	priv->object_type = schema_object_type(object_type);
> +
> +	const char *data = tuple_field(tuple, BOX_PRIV_FIELD_OBJECT_ID);
> +	if (data == NULL) {
> +		tnt_raise(ClientError, ER_NO_SUCH_FIELD,
> +		      BOX_PRIV_FIELD_OBJECT_ID + TUPLE_INDEX_BASE);
> +	}
> +	/*
> +	 * When granting or revoking privileges on a whole entity
> +	 * we pass empty string ('') to object_id to indicate
> +	 * grant on every object of that entity.
> +	 * So check for that first.
> +	 */
> +	switch (mp_typeof(*data)) {
> +	case MP_STR:
> +		if (mp_decode_strl(&data) == 0) {
> +			/* Entity-wide privilege. */
> +			priv->object_id = 0;
> +			priv->object_type = schema_entity_type(priv->object_type);
> +			break;
> +		}
> +		FALLTHROUGH;
> +	default:
> +		priv->object_id = tuple_field_u32_xc(tuple,
> +						     BOX_PRIV_FIELD_OBJECT_ID);
> +	}
>  	if (priv->object_type == SC_UNKNOWN) {
>  		tnt_raise(ClientError, ER_UNKNOWN_SCHEMA_OBJECT,
>  			  object_type);

> +enum schema_object_type
> +schema_entity_type(enum schema_object_type type)
> +{
> +	switch (type) {
> +	case SC_SPACE:
> +		return SC_ENTITY_SPACE;
> +	case SC_FUNCTION:
> +		return SC_ENTITY_FUNCTION;
> +	case SC_USER:
> +		return SC_ENTITY_USER;
> +	case SC_ROLE:
> +		return SC_ENTITY_ROLE;
> +	case SC_SEQUENCE:
> +		return SC_ENTITY_SEQUENCE;
> +	case SC_COLLATION:
> +		return SC_ENTITY_COLLATION;
> +	default:
> +		unreachable();
> +	}
> +}

You'll reach this unreachable() if you insert something like this into
_priv table

  box.space._priv:insert{1, 1, 'abc', '', 1}

I guess you should return SC_UNKNOWN instead.

Also, after looking at patch 2, I'm convinced SC_ENTITY_USER and
SC_ENTITY_ROLE should be defined there, not in this patch. Please
rebase.

Also, there's priv_def_check(), which I missed by doing review last
time. This function handles object_id == 0 for all object types, but
there's no need to anymore, because you have entity types now. Please
remove. You should add handling of entity types to that function in
this patch (you do it in patch 2 for some reason).

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

* Re: [tarantool-patches] [PATCH 2/4] Add entities user, role to access control.
  2018-08-20  8:10 ` [tarantool-patches] [PATCH 2/4] Add entities user, role to access control Serge Petrenko
@ 2018-08-22 10:37   ` Vladimir Davydov
  2018-08-22 12:53   ` Vladimir Davydov
  1 sibling, 0 replies; 12+ messages in thread
From: Vladimir Davydov @ 2018-08-22 10:37 UTC (permalink / raw)
  To: Serge Petrenko; +Cc: kostja, tarantool-patches

On Mon, Aug 20, 2018 at 11:10:06AM +0300, Serge Petrenko wrote:
> diff --git a/src/box/alter.cc b/src/box/alter.cc
> index 42136b7df..436827a6d 100644
> --- a/src/box/alter.cc
> +++ b/src/box/alter.cc
> @@ -2170,7 +2170,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;
> @@ -2179,7 +2179,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,

There's one more call to access_check_ddl() in this funciton, in the
UPDATE case. I guess you should replace SC_USER with old_user->type
there too.

> @@ -2670,6 +2670,38 @@ priv_def_check(struct priv_def *priv, enum priv_type priv_type)
>  		}
>  		/* Not necessary to do during revoke, but who cares. */
>  		role_check(grantee, role);
> +		break;
> +	}
> +	case SC_USER:
> +	{
> +		struct user *user = NULL;
> +		user = user_by_id(priv->object_id);

Nit:

		struct user *user = user_by_id(priv->object_id);

> +		if (user == NULL || user->def->type != SC_USER) {
> +			tnt_raise(ClientError, ER_NO_SUCH_USER,
> +				  user ? user->def->name :
> +				  int2str(priv->object_id));
> +		}
> +		if (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);
> +		}

I think you could painlessly merge this condition with SC_ROLE above.
Not sure if it's worth it though.

> +		break;
> +	}
> +	case SC_ENTITY_SPACE:
> +	case SC_ENTITY_FUNCTION:
> +	case SC_ENTITY_SEQUENCE:
> +	case SC_ENTITY_ROLE:
> +	case SC_ENTITY_USER:
> +	{
> +		/* Only amdin may grant privileges on an entire entity. */
> +		if (grantor->def->uid != ADMIN) {
> +			tnt_raise(AccessDeniedError, priv_name(priv_type),
> +				  schema_object_name(priv->object_type), name,
> +				  grantor->def->name);
> +		}

This should go to patch 1. This patch should only add
SC_ENTITY_USER/ROLE here.

>  	}
>  	default:
>  		break;
> @@ -1845,19 +1847,24 @@ 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
> +        if object_name == '' then
> +            return ''
> +        end
>          local _vuser = box.space[box.schema.VUSER_ID]
> -        local role
> +        local role_or_user
>          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

Nit: tab instead of spaces.

> diff --git a/src/box/user.cc b/src/box/user.cc
> index eec785652..b4fb65a59 100644
> --- a/src/box/user.cc
> +++ b/src/box/user.cc
> @@ -236,6 +246,16 @@ access_find(struct priv_def *priv)
>  			access = func->access;
>  		break;
>  	}
> +	case SC_USER:
> +	{
> +		/* No grants on a single object user yet. */
> +		break;
> +	}
> +	case SC_ROLE:
> +	{
> +		/* No grants on a single object role yet. */
> +		break;
> +	}

Again, these two could be painlessly merged, I guess. And it wouldn't
hurt patch 3 IMO. Up to you.

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

* Re: [tarantool-patches] [PATCH 3/4] Add single object privilege checks to access_check_ddl.
  2018-08-20  8:10 ` [tarantool-patches] [PATCH 3/4] Add single object privilege checks to access_check_ddl Serge Petrenko
@ 2018-08-22 11:58   ` Vladimir Davydov
  0 siblings, 0 replies; 12+ messages in thread
From: Vladimir Davydov @ 2018-08-22 11:58 UTC (permalink / raw)
  To: Serge Petrenko; +Cc: kostja, tarantool-patches

On Mon, Aug 20, 2018 at 11:10:07AM +0300, Serge Petrenko wrote:
> diff --git a/src/box/alter.cc b/src/box/alter.cc
> index 436827a6d..b58832612 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,

Nit: this could fit in one line.

>  		 enum schema_object_type type,
>  		 enum priv_type priv_type,

This too.

>  		 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;

AFAIU in case of index creation you need to check PRIV_C on the space,
no?

> +	/* 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;
> +	}

Why don't you use access_find() here? That would reduce the amount of
code so that you wouldn't need to use the error label.

> +	if (access == 0)
> +	    return; /* Access granted. */

Nit: spaces instead of a tab.

>  
>  	/* 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) {
> diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
> index bad35b680..1c254fe1d 100644
> --- a/src/box/lua/schema.lua
> +++ b/src/box/lua/schema.lua
> @@ -2060,6 +2060,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

Nit:

       -- Grant privilege 'alter to itself so that 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
> @@ -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.

Please don't mention core function names in the tests, because we can
rename them any time, which will make it more difficult to understand
the tests.

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

* Re: [tarantool-patches] [PATCH 1/4] Introduce separate entity object types for entity privileges.
  2018-08-20  8:10 ` [tarantool-patches] [PATCH 1/4] Introduce separate entity object types for entity privileges Serge Petrenko
  2018-08-22 10:28   ` Vladimir Davydov
@ 2018-08-22 12:37   ` Vladimir Davydov
  1 sibling, 0 replies; 12+ messages in thread
From: Vladimir Davydov @ 2018-08-22 12:37 UTC (permalink / raw)
  To: Serge Petrenko; +Cc: kostja, tarantool-patches

On Mon, Aug 20, 2018 at 11:10:05AM +0300, Serge Petrenko wrote:
> diff --git a/src/box/alter.cc b/src/box/alter.cc
> index 3007a131d..42136b7df 100644
> --- a/src/box/alter.cc
> +++ b/src/box/alter.cc
> @@ -2537,10 +2537,35 @@ priv_def_create_from_tuple(struct priv_def *priv, struct tuple *tuple)
>  {
>  	priv->grantor_id = tuple_field_u32_xc(tuple, BOX_PRIV_FIELD_ID);
>  	priv->grantee_id = tuple_field_u32_xc(tuple, BOX_PRIV_FIELD_UID);
> +
>  	const char *object_type =
>  		tuple_field_cstr_xc(tuple, BOX_PRIV_FIELD_OBJECT_TYPE);
> -	priv->object_id = tuple_field_u32_xc(tuple, BOX_PRIV_FIELD_OBJECT_ID);
>  	priv->object_type = schema_object_type(object_type);
> +
> +	const char *data = tuple_field(tuple, BOX_PRIV_FIELD_OBJECT_ID);
> +	if (data == NULL) {
> +		tnt_raise(ClientError, ER_NO_SUCH_FIELD,
> +		      BOX_PRIV_FIELD_OBJECT_ID + TUPLE_INDEX_BASE);

Nit: bad indentation, please align this line to the parenthesis.

> diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua
> index 0293f6ef8..091da2dc4 100644
> --- a/src/box/lua/upgrade.lua
> +++ b/src/box/lua/upgrade.lua
> @@ -964,6 +964,28 @@ local function upgrade_to_1_10_0()
>      create_vsequence_space()
>  end
>  
> +--------------------------------------------------------------------------------
> +--- Tarantool 1.10.2
> +--------------------------------------------------------------------------------
> +local function upgrade_space_priv_to_1_10_2()

Let's rename this function to upgrade_priv_to_1_10_2()

> +    local _priv = box.space._priv
> +    local _vpriv = box.space._vpriv
> +    local f = _priv:format()

Nit: please rename 'f' to 'format'.

> +
> +    f[4].type = 'scalar'
> +    _priv:format(f)
> +    f = _vpriv:format()
> +    f[4].type = 'scalar'
> +    _vpriv:format(f)
> +    _priv.index.primary:alter{parts={2, 'unsigned', 3, 'string', 4, 'scalar'}}
> +    _vpriv.index.primary:alter{parts={2, 'unsigned', 3, 'string', 4, 'scalar'}}
> +    _priv.index.object:alter{parts={3, 'string', 4, 'scalar'}}
> +    _vpriv.index.object:alter{parts={3, 'string', 4, 'scalar'}}
> +end
> +
> +local function upgrade_to_1_10_2()
> +    upgrade_space_priv_to_1_10_2()
> +end

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

* Re: [tarantool-patches] [PATCH 4/4] Add a privilege upgrade script and update tests.
  2018-08-20  8:10 ` [tarantool-patches] [PATCH 4/4] Add a privilege upgrade script and update tests Serge Petrenko
@ 2018-08-22 12:48   ` Vladimir Davydov
  0 siblings, 0 replies; 12+ messages in thread
From: Vladimir Davydov @ 2018-08-22 12:48 UTC (permalink / raw)
  To: Serge Petrenko; +Cc: kostja, tarantool-patches

On Mon, Aug 20, 2018 at 11:10:08AM +0300, Serge Petrenko wrote:
> This patch adds a privilege upgrade script, which runs on upgrade to
> 1.10.2 and automatically grants CREATE,ALTER,DROP on objects and entities
> to all users, who have READ and WRITE access on them.

You modify the upgrade script, but not access_check_ddl() - the latter
will still treat read,write as create,alter,drop. Now, suppose the user
will grant read,write to universe *after* upgrading to 1.10.2.
Everything's going to work fine until we disable legacy mode and start
claiming that create,alter,drop are set explicitly. And there will be no
upgrade script to fix that. That said, if you change the upgrade script
you should also remove the legacy mode from access_check_ddl.

> Also all tests are rewritten to grant only necessary privileges, not
> privileges to universe.

AFAIU the tests you're patching have nothing to do with this particular
problem. Please submit them separately.

> diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua
> index 091da2dc4..d8d03288c 100644
> --- a/src/box/lua/upgrade.lua
> +++ b/src/box/lua/upgrade.lua
> @@ -983,8 +983,31 @@ local function upgrade_space_priv_to_1_10_2()
>      _vpriv.index.object:alter{parts={3, 'string', 4, 'scalar'}}
>  end
>  
> +local function upgrade_users_to_1_10_2()

I don't think it's worth factoring this code out to a separate function.
Let's just fold it into upgrade_priv_to_1_10_2, because after all it's
about updating privileges.

> +    local _priv = box.space[box.schema.PRIV_ID]
> +    local _user = box.space[box.schema.USER_ID]
> +

Please add a comprehensive comment explaining what you're doing here
and why. Don't forget to mention why you need to handle 'universe' in
a special way.

> +    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)
> +                    -- for universal grants

This particular comment is useless. Please remove.

> +                    if priv[3] == 'universe' 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_2()
>      upgrade_space_priv_to_1_10_2()
> +    upgrade_users_to_1_10_2()
>  end
>  
>  local function get_version()

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

* Re: [tarantool-patches] [PATCH 2/4] Add entities user, role to access control.
  2018-08-20  8:10 ` [tarantool-patches] [PATCH 2/4] Add entities user, role to access control Serge Petrenko
  2018-08-22 10:37   ` Vladimir Davydov
@ 2018-08-22 12:53   ` Vladimir Davydov
  1 sibling, 0 replies; 12+ messages in thread
From: Vladimir Davydov @ 2018-08-22 12:53 UTC (permalink / raw)
  To: Serge Petrenko; +Cc: kostja, tarantool-patches

On Mon, Aug 20, 2018 at 11:10:06AM +0300, Serge Petrenko wrote:
> 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
> +

Hmm, why? I don't understand how this change is connected to this patch.

>  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')

This is OK, I guess.

>  box.schema.user.grant('tester', 'read', 'space', '_sequence')
>  box.session.su("tester")
>  -- successful create

> 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')

IMO this belongs to the patch that will fix *all* access tests (it
should be separated from patch 4).

>  box.session.su('user')
>  box.schema.role.create('role')
>  box.session.su('admin')

^ permalink raw reply	[flat|nested] 12+ 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 ` Serge Petrenko
  0 siblings, 0 replies; 12+ 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] 12+ messages in thread

end of thread, other threads:[~2018-08-22 12:53 UTC | newest]

Thread overview: 12+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2018-08-20  8:10 [tarantool-patches] [PATCH 0/4] Finish implementation of privileges Serge Petrenko
2018-08-20  8:10 ` [tarantool-patches] [PATCH 1/4] Introduce separate entity object types for entity privileges Serge Petrenko
2018-08-22 10:28   ` Vladimir Davydov
2018-08-22 12:37   ` Vladimir Davydov
2018-08-20  8:10 ` [tarantool-patches] [PATCH 2/4] Add entities user, role to access control Serge Petrenko
2018-08-22 10:37   ` Vladimir Davydov
2018-08-22 12:53   ` Vladimir Davydov
2018-08-20  8:10 ` [tarantool-patches] [PATCH 3/4] Add single object privilege checks to access_check_ddl Serge Petrenko
2018-08-22 11:58   ` Vladimir Davydov
2018-08-20  8:10 ` [tarantool-patches] [PATCH 4/4] Add a privilege upgrade script and update tests Serge Petrenko
2018-08-22 12:48   ` Vladimir Davydov
  -- strict thread matches above, loose matches on Subject: below --
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 3/4] Add single object privilege checks to access_check_ddl Serge Petrenko

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