diff --git a/src/box/alter.cc b/src/box/alter.cc index c0d6312c3a5f220f36b211d8b42446e4102fe2bb..1f55dc9fa71d4652884b98f656827b3e2ad4906e 100644 --- a/src/box/alter.cc +++ b/src/box/alter.cc @@ -74,8 +74,13 @@ access_check_ddl(uint32_t owner_uid) { struct credentials *cr = current_user(); /* - * Only the creator of the space or superuser can modify - * the space, since we don't have ALTER privilege. + * For privileges, only the current user can claim he's + * the grantor/owner of the privilege that is being + * granted. + * For spaces/funcs/other objects, only the creator + * of the object or admin can modify the space, since + * there is no such thing in Tarantool as GRANT OPTION or + * ALTER privilege. */ if (owner_uid != cr->uid && cr->uid != ADMIN) { struct user *user = user_cache_find(cr->uid); @@ -1448,9 +1453,9 @@ priv_def_create_from_tuple(struct priv_def *priv, struct tuple *tuple) static void priv_def_check(struct priv_def *priv) { - struct user_def *grantor = user_cache_find(priv->grantor_id); + struct user *grantor = user_cache_find(priv->grantor_id); /* May be a role */ - struct user_def *grantee = user_by_id(priv->grantee_id); + struct user *grantee = user_by_id(priv->grantee_id); if (grantee == NULL) { tnt_raise(ClientError, ER_NO_SUCH_USER, int2str(priv->grantee_id)); @@ -1481,6 +1486,21 @@ priv_def_check(struct priv_def *priv) } break; } + case SC_ROLE: + { + struct user *role = user_by_id(priv->object_id); + if (role == NULL || role->type != SC_ROLE) { + tnt_raise(ClientError, ER_NO_SUCH_ROLE, + role ? role->name : + int2str(priv->object_id)); + } + /* Only the creator of the role can grant it. */ + if (role->owner != grantor->uid && grantor->uid != ADMIN) { + tnt_raise(ClientError, ER_ACCESS_DENIED, + role->name, grantor->name); + } + role_check(role, grantee); + } default: break; } @@ -1500,7 +1520,7 @@ grant_or_revoke(struct priv_def *priv) switch (priv->object_type) { case SC_UNIVERSE: { - access = &grantee->universal_access; + access = universe.access; /** Update cache at least in the current session. */ struct credentials *cr = current_user(); if (grantee->uid == cr->uid) @@ -1511,21 +1531,34 @@ grant_or_revoke(struct priv_def *priv) { struct space *space = space_by_id(priv->object_id); if (space) - access = &space->access[grantee->auth_token]; + access = space->access; break; } case SC_FUNCTION: { struct func_def *func = func_by_id(priv->object_id); if (func) - access = &func->access[grantee->auth_token]; + access = func->access; + break; + } + case SC_ROLE: + { + struct user *role = user_by_id(priv->object_id); + if (role == NULL || role->type != SC_ROLE) + break; + if (priv->access) + role_grant(grantee, role); + else + role_revoke(grantee, role); break; } default: break; } - if (access) - access->granted = access->effective = priv->access; + if (access) { + access[grantee->auth_token].granted = priv->access; + access[grantee->auth_token].effective = priv->access; + } } /** A trigger called on rollback of grant, or on commit of revoke. */ @@ -1538,6 +1571,12 @@ revoke_priv(struct trigger * /* trigger */, void *event) stmt->new_tuple : stmt->old_tuple); struct priv_def priv; priv_def_create_from_tuple(&priv, tuple); + /* + * Access to the object has been removed altogether so + * there should be no grants at all. If only some grants + * were removed, modify_priv trigger would have been + * invoked. + */ priv.access = 0; grant_or_revoke(&priv); } diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua index 27865775ce42ec366516ddb6f6f116a2dce2926b..a5d0c6ff268b01c05d113fe870382ea72d05c8ae 100644 --- a/src/box/lua/schema.lua +++ b/src/box/lua/schema.lua @@ -1021,6 +1021,15 @@ end box.schema.user.grant = function(user_name, privilege, object_type, object_name, grantor) + -- 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' + end local uid = user_resolve(user_name) if uid == nil then box.error(box.error.NO_SUCH_USER, user_name) @@ -1050,6 +1059,13 @@ box.schema.user.grant = function(user_name, privilege, object_type, end box.schema.user.revoke = function(user_name, privilege, object_type, object_name) + -- 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' + end local uid = user_resolve(user_name) if uid == nil then box.error(box.error.NO_SUCH_USER, name) diff --git a/src/box/session.h b/src/box/session.h index 77d1fb5a128aec88db1c32419d8b5927221e2ad0..72107541a9cd065e36ed50ce6ba5cb50fb83b470 100644 --- a/src/box/session.h +++ b/src/box/session.h @@ -183,7 +183,7 @@ static inline void credentials_init(struct credentials *cr, struct user *user) { cr->auth_token = user->auth_token; - cr->universal_access = user->universal_access.effective; + cr->universal_access = universe.access[cr->auth_token].effective; cr->uid = user->uid; } diff --git a/src/box/user.cc b/src/box/user.cc index 8334b30c06e9571fa01dd448f6dcfa0c7f385e56..61cf2a5f86ba4bec52f82be20fb7dd29472e2434 100644 --- a/src/box/user.cc +++ b/src/box/user.cc @@ -30,63 +30,161 @@ #include "user_def.h" #include "assoc.h" #include "schema.h" +#include "bit/bit.h" +struct universe universe; static struct user users[BOX_USER_MAX]; struct user *guest_user = users; struct user *admin_user = users + 1; -/** Bitmap type for used/unused authentication token map. */ -typedef unsigned long user_map_t; +struct mh_i32ptr_t *user_registry; -/** A map to quickly look up free slots in users[] array. */ -user_map_t user_map[BOX_USER_MAX/(CHAR_BIT*sizeof(user_map_t)) + 1]; +/* {{{ user_map */ -int user_map_idx = 0; -struct mh_i32ptr_t *user_registry; +/* Initialize an empty user map. */ +void +user_map_init(struct user_map *map) +{ + memset(map, 0, sizeof(*map)); +} -uint8_t -user_map_get_slot() +static inline int +user_map_calc_idx(uint8_t auth_token, uint8_t *bit_no) { - uint32_t idx = __builtin_ffsl(user_map[user_map_idx]); - while (idx == 0) { - if (user_map_idx == sizeof(user_map)/sizeof(*user_map)) - panic("Out of slots for new users"); + *bit_no = auth_token & (UMAP_INT_BITS - 1); + return auth_token / UMAP_INT_BITS; +} - user_map_idx++; - idx = __builtin_ffsl(user_map[user_map_idx]); + +/** Set a bit in the user map - add a user. */ +static inline void +user_map_set(struct user_map *map, uint8_t auth_token) +{ + uint8_t bit_no; + int idx = user_map_calc_idx(auth_token, &bit_no); + map->m[idx] |= ((umap_int_t) 1) << bit_no; +} + +/** Clear a bit in the user map - remove a user. */ +static inline void +user_map_clear(struct user_map *map, uint8_t auth_token) +{ + uint8_t bit_no; + int idx = user_map_calc_idx(auth_token, &bit_no); + map->m[idx] &= ~(((umap_int_t) 1) << bit_no); +} + +/* Check if a bit is set in the user map. */ +static inline bool +user_map_is_set(struct user_map *map, uint8_t auth_token) +{ + uint8_t bit_no; + int idx = user_map_calc_idx(auth_token, &bit_no); + return map->m[idx] & (((umap_int_t) 1) << bit_no); +} + +/** + * Merge two sets of users: add all users from right argument + * to the left one. + */ +void +user_map_union(struct user_map *lhs, struct user_map *rhs) +{ + for (int i = 0; i < USER_MAP_SIZE; i++) + lhs->m[i] |= rhs->m[i]; +} + +/** Iterate over users in the set of users. */ +struct user_map_iterator +{ + struct bit_iterator it; +}; + +void +user_map_iterator_init(struct user_map_iterator *it, struct user_map *map) +{ + bit_iterator_init(&it->it, map->m, + USER_MAP_SIZE * sizeof(umap_int_t), true); +} + +struct user * +user_map_iterator_next(struct user_map_iterator *it) +{ + size_t auth_token = bit_iterator_next(&it->it); + if (auth_token != SIZE_MAX) + return users + auth_token; + return NULL; +} + +/* }}} */ + +/* {{{ authentication tokens */ + +/** A map to quickly look up free slots in users[] array. */ +static umap_int_t tokens[USER_MAP_SIZE]; +/** + * Index of the minimal element of the tokens array which + * has an unused token. + */ +static int min_token_idx = 0; + +/** + * Find and return a spare authentication token. + * Raise an exception when the maximal number of users + * is reached (and we're out of tokens). + */ +uint8_t +auth_token_get() +{ + uint8_t bit_no = 0; + while (min_token_idx < USER_MAP_SIZE) { + bit_no = __builtin_ffs(tokens[min_token_idx]); + if (bit_no) + break; + min_token_idx++; } + if (bit_no == 0 || bit_no > BOX_USER_MAX) { + /* A cap on the number of users was reached. + * Check for BOX_USER_MAX to cover case when + * USER_MAP_BITS > BOX_USER_MAX. + */ + tnt_raise(LoggedError, ER_USER_MAX, BOX_USER_MAX); + } /* * find-first-set returns bit index starting from 1, * or 0 if no bit is set. Rebase the index to offset 0. */ - idx--; - if (idx == BOX_USER_MAX) { - /* A cap on the number of users was reached. */ - tnt_raise(LoggedError, ER_USER_MAX, BOX_USER_MAX); - } - user_map[user_map_idx] ^= ((user_map_t) 1) << idx; - idx += user_map_idx * sizeof(*user_map) * CHAR_BIT; - assert(idx < UINT8_MAX); - return idx; + bit_no--; + tokens[min_token_idx] ^= ((umap_int_t) 1) << bit_no; + int auth_token = min_token_idx * UMAP_INT_BITS + bit_no; + assert(auth_token < UINT8_MAX); + return auth_token; } +/** + * Return an authentication token to the set of unused + * tokens. + */ void -user_map_put_slot(uint8_t auth_token) +auth_token_put(uint8_t auth_token) { - memset(users + auth_token, 0, sizeof(struct user_def)); - uint32_t bit_no = auth_token & (sizeof(user_map_t) * CHAR_BIT - 1); - auth_token /= sizeof(user_map_t) * CHAR_BIT; - user_map[auth_token] |= ((user_map_t) 1) << bit_no; - if (auth_token > user_map_idx) - user_map_idx = auth_token; + uint8_t bit_no; + int idx = user_map_calc_idx(auth_token, &bit_no); + tokens[idx] |= ((umap_int_t) 1) << bit_no; + if (idx < min_token_idx) + min_token_idx = idx; } +/* }}} */ + +/* {{{ user cache */ + struct user * user_cache_replace(struct user_def *def) { struct user *user = user_by_id(def->uid); if (user == NULL) { - uint8_t auth_token = user_map_get_slot(); + uint8_t auth_token = auth_token_get(); user = users + auth_token; assert(user->auth_token == 0); user->auth_token = auth_token; @@ -100,11 +198,17 @@ user_cache_replace(struct user_def *def) void user_cache_delete(uint32_t uid) { - struct user *old = user_by_id(uid); - if (old) { - assert(old->auth_token > ADMIN); - user_map_put_slot(old->auth_token); - memset(old, 0, sizeof(*old)); + struct user *user = user_by_id(uid); + if (user) { + assert(user->auth_token > ADMIN); + auth_token_put(user->auth_token); + /* + * Sic: we don't have to remove a deleted + * user from users hash of roles, since + * to drop a user, one has to revoke + * all privileges from them first. + */ + memset(user, 0, sizeof(*user)); mh_i32ptr_del(user_registry, uid, NULL); } } @@ -146,7 +250,8 @@ user_cache_find_by_name(const char *name, uint32_t len) void user_cache_init() { - memset(user_map, 0xFF, sizeof(user_map)); + /** Mark all tokens as unused. */ + memset(tokens, 0xFF, sizeof(tokens)); user_registry = mh_i32ptr_new(); /* * Solve a chicken-egg problem: @@ -180,3 +285,69 @@ user_cache_free() if (user_registry) mh_i32ptr_delete(user_registry); } + +/* }}} user cache */ + +/** {{{ roles */ + +void +role_check(struct user *role, struct user *grantee) +{ + /* + * Check that there is no loop from grantee to role: + * if grantee is a role, build up a closure of all + * immediate and indirect users of grantee, and ensure + * the granted role is not in this set. + */ + struct user_map transitive_closure; + user_map_init(&transitive_closure); + user_map_set(&transitive_closure, grantee->auth_token); + struct user_map current_layer = transitive_closure; + while (true) { + /* + * As long as we're traversing a directed + * acyclic graph, we're bound to end at some + * point in a layer with no incoming edges. + */ + struct user_map next_layer; + user_map_init(&next_layer); + bool found = false; + struct user_map_iterator it; + user_map_iterator_init(&it, ¤t_layer); + struct user *user; + while ((user = user_map_iterator_next(&it))) { + user_map_union(&next_layer, &user->users); + found = true; + } + user_map_union(&transitive_closure, &next_layer); + if (! found) + break; + current_layer = next_layer; + } + + if (user_map_is_set(&transitive_closure, + role->auth_token)) { + tnt_raise(ClientError, ER_ROLE_LOOP, + role->name, grantee->name); + } +} + +void +role_grant(struct user *grantee, struct user *role) +{ + user_map_set(&role->users, grantee->auth_token); +} + +void +role_revoke(struct user *grantee, struct user *role) +{ + user_map_clear(&role->users, grantee->auth_token); +} + +void +user_set_effective_access(struct user * /* grantee */, + struct user * /* role */) +{ +} + +/** }}} */ diff --git a/src/box/user.h b/src/box/user.h index 2397639cdb8d392a31a31f0433d1bb92ee26d0ef..dfb0d551ab2931f2d79369a2a0705699f084a44e 100644 --- a/src/box/user.h +++ b/src/box/user.h @@ -31,18 +31,37 @@ #include <stdint.h> #include "user_def.h" +/** Global grants. */ +struct universe { + /** Global privileges this user has on the universe. */ + struct access access[BOX_USER_MAX]; +}; + +/** A single instance of the universe. */ +extern struct universe universe; + +/** Bitmap type for used/unused authentication token map. */ +typedef unsigned int umap_int_t; +enum { + UMAP_INT_BITS = CHAR_BIT * sizeof(umap_int_t), + USER_MAP_SIZE = (BOX_USER_MAX + UMAP_INT_BITS - 1)/UMAP_INT_BITS +}; + +struct user_map { + umap_int_t m[USER_MAP_SIZE]; +}; + struct user: public user_def { - /** Global privileges this user has on the universe. */ - struct access universal_access; /** * An id in privileges array to quickly find a * respective privilege. */ uint8_t auth_token; + /** List of users or roles this role has been granted to */ + struct user_map users; }; - /** * For best performance, all users are maintained in this array. * Position in the array is store in user->auth_token and also @@ -97,4 +116,36 @@ user_cache_init(); void user_cache_free(); +/* {{{ Roles */ + +/** + * Check, mainly, that users & roles form an acyclic graph, + * and no loop in the graph will occur when grantee gets + * a given role. + */ +void +role_check(struct user *role, struct user *grantee); + +/** + * Grant a role to a user or another role. + */ +void +role_grant(struct user *grantee, struct user *role); + +/** + * Revoke a role from a user or another role. + */ +void +role_revoke(struct user *grantee, struct user *role); + +/** + * Re-evaluate effective access of a user based on this role + * that is granted to him/her. + * The role has its effective access already re-evaluated. + */ +void +user_set_effective_access(struct user *grantee, struct user *role); + +/* }}} */ + #endif /* INCLUDES_TARANTOOL_BOX_USER_H */ diff --git a/src/errcode.h b/src/errcode.h index b010f0e82d214aaede399a87ee6d1875011115f0..a89a9c34efbd14e772b5ea49446ac85474bc94ad 100644 --- a/src/errcode.h +++ b/src/errcode.h @@ -135,7 +135,8 @@ enum { TNT_ERRMSG_MAX = 512 }; /* 83 */_(ER_ROLE_EXISTS, 2, "Role '%s' already exists") \ /* 84 */_(ER_CREATE_ROLE, 2, "Failed to create role '%s': %s") \ /* 85 */_(ER_INDEX_EXISTS, 2, "Index '%s' already exists") \ - /* 86 */_(ER_TUPLE_REF_OVERFLOW, 1, "Tuple reference counter is overflowed") \ + /* 86 */_(ER_TUPLE_REF_OVERFLOW, 1, "Tuple reference counter overflow") \ + /* 87 */_(ER_ROLE_LOOP, 2, "Granting role '%s' to role '%s' would create a loop") \ /* * !IMPORTANT! Please follow instructions at start of the file diff --git a/test/box/misc.result b/test/box/misc.result index 247bff00e6063cc1e0ee8555a69c63a709727b5b..249ece55c017ef29225019125d319d8d392c60c8 100644 --- a/test/box/misc.result +++ b/test/box/misc.result @@ -195,6 +195,7 @@ t; - 'box.error.UNKNOWN_SERVER : 62' - 'box.error.FUNCTION_EXISTS : 52' - 'box.error.NO_SUCH_FUNCTION : 51' + - 'box.error.ROLE_LOOP : 87' - 'box.error.TUPLE_NOT_FOUND : 4' - 'box.error.DROP_USER : 44' - 'box.error.CROSS_ENGINE_TRANSACTION : 81' diff --git a/test/box/role.result b/test/box/role.result index aae49703b603f01a4af9bd9c401979bfd03a52be..37f2f9f9c0e20b06eb289c4db823e61ef293b79b 100644 --- a/test/box/role.result +++ b/test/box/role.result @@ -69,4 +69,67 @@ box.schema.user.grant('tester', 'read', 'role', 'iddqd') -- test granting role to a role box.schema.user.grant('iddqd', 'execute', 'role', 'iddqd') --- +- error: Granting role 'iddqd' to role 'iddqd' would create a loop +... +box.schema.user.grant('iddqd', 'iddqd') +--- +- error: Granting role 'iddqd' to role 'iddqd' would create a loop +... +box.schema.user.revoke('iddqd', 'iddqd') +--- +... +box.schema.user.grant('tester', 'iddqd') +--- +... +box.schema.user.revoke('tester', 'iddqd') +--- +... +box.schema.user.revoke('tester', 'no-such-role') +--- +- error: Role 'no-such-role' is not found +... +box.schema.user.grant('tester', 'no-such-role') +--- +- error: Role 'no-such-role' is not found +... +-- check for loops in role grants +box.schema.role.create('a') +--- +... +box.schema.role.create('b') +--- +... +box.schema.role.create('c') +--- +... +box.schema.role.create('d') +--- +... +box.schema.user.grant('b', 'a') +--- +... +box.schema.user.grant('c', 'a') +--- +... +box.schema.user.grant('d', 'b') +--- +... +box.schema.user.grant('d', 'c') +--- +... +box.schema.user.grant('a', 'd') +--- +- error: Granting role 'd' to role 'a' would create a loop +... +box.schema.role.drop('d') +--- +... +box.schema.role.drop('b') +--- +... +box.schema.role.drop('c') +--- +... +box.schema.role.drop('a') +--- ... diff --git a/test/box/role.test.lua b/test/box/role.test.lua index 9d63d60331337d8dd78be4c1a1d5f84f38023356..fd08324200406c4c7cdd8bcaf3859b226681cda1 100644 --- a/test/box/role.test.lua +++ b/test/box/role.test.lua @@ -22,3 +22,23 @@ box.schema.user.grant('tester', 'write', 'role', 'iddqd') box.schema.user.grant('tester', 'read', 'role', 'iddqd') -- test granting role to a role box.schema.user.grant('iddqd', 'execute', 'role', 'iddqd') +box.schema.user.grant('iddqd', 'iddqd') +box.schema.user.revoke('iddqd', 'iddqd') +box.schema.user.grant('tester', 'iddqd') +box.schema.user.revoke('tester', 'iddqd') +box.schema.user.revoke('tester', 'no-such-role') +box.schema.user.grant('tester', 'no-such-role') +-- check for loops in role grants +box.schema.role.create('a') +box.schema.role.create('b') +box.schema.role.create('c') +box.schema.role.create('d') +box.schema.user.grant('b', 'a') +box.schema.user.grant('c', 'a') +box.schema.user.grant('d', 'b') +box.schema.user.grant('d', 'c') +box.schema.user.grant('a', 'd') +box.schema.role.drop('d') +box.schema.role.drop('b') +box.schema.role.drop('c') +box.schema.role.drop('a') diff --git a/test/box/select.result b/test/box/select.result index 82fa43b53777d292e62d6c1ebd6b6cd9526bfa2c..44ce0acc7cadd555fa304fad7c6d9719612200a4 100644 --- a/test/box/select.result +++ b/test/box/select.result @@ -555,7 +555,7 @@ ref_count = 0 ... while (true) do table.insert(lots_of_links, s:get{0}) ref_count = ref_count + 1 end --- -- error: Tuple reference counter is overflowed +- error: Tuple reference counter overflow ... ref_count ---