From bb830934fe144d1c452f771ecf7151e39d3b358c Mon Sep 17 00:00:00 2001 From: landa Date: Wed, 24 Jun 2026 23:35:51 +0300 Subject: [PATCH] core: tube grant for roles Adds ability to grant tubes for roles. Users ability to grant tubes is not affected. --- CHANGELOG.md | 2 + queue/abstract.lua | 90 ++++++--- queue/abstract/driver/fifo.lua | 4 + queue/abstract/driver/fifottl.lua | 4 + queue/abstract/driver/utube.lua | 8 + queue/abstract/driver/utubettl.lua | 8 + queue/abstract/queue_session.lua | 8 + t/090-grant-check.t | 139 ++++++++++++- t/240-grant-via-role.t | 315 +++++++++++++++++++++++++++++ 9 files changed, 545 insertions(+), 33 deletions(-) create mode 100755 t/240-grant-via-role.t diff --git a/CHANGELOG.md b/CHANGELOG.md index 07ac4d4e..1e89f96a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added +- Ability to grant tubes to roles. All default tube drivers are supported (#249). + ### Changed ### Fixed diff --git a/queue/abstract.lua b/queue/abstract.lua index 9c2d4342..e49a335a 100644 --- a/queue/abstract.lua +++ b/queue/abstract.lua @@ -324,51 +324,79 @@ function tube.on_task_change(self, cb) return old_cb end -function tube.grant(self, user, args) - if not check_state("grant") then - return +local function tube_grant_space(grantor, grantee, name, tp) + grantor.grant(grantee, tp or 'read,write', 'space', name, { + if_not_exists = true, + }) +end + +local function tube_grant_func(grantor, grantee, name) + box.schema.func.create(name, { if_not_exists = true }) + grantor.grant(grantee, 'execute', 'function', name, { + if_not_exists = true + }) +end + +local function grant_system_spaces(grantor, grantee) + tube_grant_space(grantor, grantee, '_queue', 'read') + tube_grant_space(grantor, grantee, '_queue_consumers') + tube_grant_space(grantor, grantee, '_queue_taken_2') +end + +local function grant_args(grantor, grantee, args, name) + if args.call then + tube_grant_func(grantor, grantee, 'queue.identify') + tube_grant_func(grantor, grantee, 'queue.statistics') + local prefix = (args.prefix or 'queue.tube') .. ('.%s:'):format(name) + tube_grant_func(grantor, grantee, prefix .. 'put') + tube_grant_func(grantor, grantee, prefix .. 'take') + tube_grant_func(grantor, grantee, prefix .. 'touch') + tube_grant_func(grantor, grantee, prefix .. 'ack') + tube_grant_func(grantor, grantee, prefix .. 'release') + tube_grant_func(grantor, grantee, prefix .. 'peek') + tube_grant_func(grantor, grantee, prefix .. 'bury') + tube_grant_func(grantor, grantee, prefix .. 'kick') + tube_grant_func(grantor, grantee, prefix .. 'delete') end - local function tube_grant_space(user, name, tp) - box.schema.user.grant(user, tp or 'read,write', 'space', name, { - if_not_exists = true, - }) + + if args.truncate then + local prefix = (args.prefix or 'queue.tube') .. ('.%s:'):format(name) + tube_grant_func(grantor, grantee, prefix .. 'truncate') end +end - local function tube_grant_func(user, name) - box.schema.func.create(name, { if_not_exists = true }) - box.schema.user.grant(user, 'execute', 'function', name, { - if_not_exists = true - }) +function tube.grant(self, user, args) + if not check_state("grant") then + return end args = args or {} - tube_grant_space(user, '_queue', 'read') - tube_grant_space(user, '_queue_consumers') - tube_grant_space(user, '_queue_taken_2') + grant_system_spaces(box.schema.user, user) + self.raw:grant(user, {if_not_exists = true}) session.grant(user) - if args.call then - tube_grant_func(user, 'queue.identify') - tube_grant_func(user, 'queue.statistics') - local prefix = (args.prefix or 'queue.tube') .. ('.%s:'):format(self.name) - tube_grant_func(user, prefix .. 'put') - tube_grant_func(user, prefix .. 'take') - tube_grant_func(user, prefix .. 'touch') - tube_grant_func(user, prefix .. 'ack') - tube_grant_func(user, prefix .. 'release') - tube_grant_func(user, prefix .. 'peek') - tube_grant_func(user, prefix .. 'bury') - tube_grant_func(user, prefix .. 'kick') - tube_grant_func(user, prefix .. 'delete') + grant_args(box.schema.user, user, args, self.name) +end + +function tube.grant_role(self, role, args) + if not check_state("grant") then + return end - if args.truncate then - local prefix = (args.prefix or 'queue.tube') .. ('.%s:'):format(self.name) - tube_grant_func(user, prefix .. 'truncate') + args = args or {} + + if self.raw.grant_role ~= nil then + self.raw:grant_role(role, { if_not_exists = true }) + else + error(('Tube %s driver does not support grant_role()'):format(self.name)) end + grant_system_spaces(box.schema.role, role) + session.grant_role(role) + + grant_args(box.schema.role, role, args, self.name) end -- methods diff --git a/queue/abstract/driver/fifo.lua b/queue/abstract/driver/fifo.lua index 1cb65f93..b027d42f 100644 --- a/queue/abstract/driver/fifo.lua +++ b/queue/abstract/driver/fifo.lua @@ -66,6 +66,10 @@ function method.grant(self, user, opts) box.schema.user.grant(user, 'read,write', 'space', self.space.name, opts) end +function method.grant_role(self, role, opts) + box.schema.role.grant(role, 'read,write', 'space', self.space.name, opts) +end + -- normalize task: cleanup all internal fields function method.normalize_task(self, task) return task diff --git a/queue/abstract/driver/fifottl.lua b/queue/abstract/driver/fifottl.lua index e44a4768..8d07bc5b 100644 --- a/queue/abstract/driver/fifottl.lua +++ b/queue/abstract/driver/fifottl.lua @@ -207,6 +207,10 @@ function method.grant(self, user, opts) box.schema.user.grant(user, 'read,write', 'space', self.space.name, opts) end +function method.grant_role(self, role, opts) + box.schema.role.grant(role, 'read,write', 'space', self.space.name, opts) +end + -- cleanup internal fields in task function method.normalize_task(self, task) return task and task:transform(3, 5) diff --git a/queue/abstract/driver/utube.lua b/queue/abstract/driver/utube.lua index 0a7faf94..03d9ae75 100644 --- a/queue/abstract/driver/utube.lua +++ b/queue/abstract/driver/utube.lua @@ -134,6 +134,14 @@ function method.grant(self, user, opts) end end +function method.grant_role(self, role, opts) + box.schema.role.grant(role, 'read,write', 'space', self.space.name, opts) + if self.space_ready_buffer ~= nil then + box.schema.role.grant(role, 'read,write', 'space', + self.space_ready_buffer.name, opts) + end +end + -- normalize task: cleanup all internal fields function method.normalize_task(self, task) return task and task:transform(3, 1) diff --git a/queue/abstract/driver/utubettl.lua b/queue/abstract/driver/utubettl.lua index 7b726ba5..0fd1c2b5 100644 --- a/queue/abstract/driver/utubettl.lua +++ b/queue/abstract/driver/utubettl.lua @@ -397,6 +397,14 @@ function method.grant(self, user, opts) end end +function method.grant_role(self, role, opts) + box.schema.role.grant(role, 'read,write', 'space', self.space.name, opts) + if self.space_ready_buffer ~= nil then + box.schema.role.grant(role, 'read,write', 'space', + self.space_ready_buffer.name, opts) + end +end + -- cleanup internal fields in task function method.normalize_task(self, task) return task and task:transform(i_next_event, i_data - i_next_event) diff --git a/queue/abstract/queue_session.lua b/queue/abstract/queue_session.lua index 9f32a8e0..7a65e333 100644 --- a/queue/abstract/queue_session.lua +++ b/queue/abstract/queue_session.lua @@ -292,6 +292,13 @@ local function grant(user) { if_not_exists = true }) end +local function grant_role(role) + box.schema.role.grant(role, 'read, write', 'space', '_queue_session_ids', + { if_not_exists = true }) + box.schema.role.grant(role, 'read, write', 'space', '_queue_shared_sessions', + { if_not_exists = true }) +end + local function start() identification_init() queue_session.sync_chan = fiber.channel() @@ -349,6 +356,7 @@ local method = { identify = identify, disconnect = disconnect, grant = grant, + grant_role = grant_role, on_session_remove = on_session_remove, start = start, stop = stop, diff --git a/t/090-grant-check.t b/t/090-grant-check.t index 03dde415..fc083294 100755 --- a/t/090-grant-check.t +++ b/t/090-grant-check.t @@ -1,8 +1,9 @@ #!/usr/bin/env tarantool local test = require('tap').test() -test:plan(9) +test:plan(17) local test_user = 'test' +local test_role = 'test_role' local test_pass = '1234' local test_host = 'localhost' local test_port = '3388' @@ -53,7 +54,7 @@ local test_drivers_grant_cases = { } for _, tc in pairs(test_drivers_grant_cases) do - test:test('test dirvers grant ' .. tc.name, function(test) + test:test('test drivers grant ' .. tc.name, function(test) local queue = require('queue') box.schema.user.create(test_user, { password = test_pass }) @@ -81,6 +82,38 @@ for _, tc in pairs(test_drivers_grant_cases) do box.schema.user.drop(test_user) tube:drop() end) + + test:test('test drivers grant_role ' .. tc.name, function(test) + local queue = require('queue') + box.schema.user.create(test_user, { password = test_pass }) + box.schema.role.create(test_role) + box.schema.user.grant(test_user, test_role, nil, nil, { if_not_exists = true }) + + test:plan(2) + local tube_opts = { engine = engine } + if tc.storage_mode ~= nil and tc.storage_mode ~= 'default' then + tube_opts.storage_mode = tc.storage_mode + tube_opts.engine = 'memtx' + end + + local tube_name = 'test_' .. tc.name .. '_role' + local tube = queue.create_tube(tube_name, tc.queue_type, tube_opts) + tube:put('help') + + tube:grant_role(test_role) + + box.session.su(test_user, function() + local a = tube:take() + test:is(a[1], 0, 'take works via role grants') + + local c = tube:ack(a[1]) + test:is(c[1], 0, 'ack works via role grants') + end) + + tube:drop() + box.schema.user.drop(test_user) + box.schema.role.drop(test_role) + end) end test:test('check for space grants', function(test) @@ -120,6 +153,43 @@ test:test('check for space grants', function(test) tube:drop() end) +test:test('check for space grants via role', function(test) + local queue = require('queue') + box.schema.user.create(test_user, { password = test_pass }) + box.schema.role.create(test_role) + box.schema.user.grant(test_user, test_role, nil, nil, { if_not_exists = true }) + + test:plan(5) + + local tube = queue.create_tube('test', 'fifo', { engine = engine }) + tube:put('help') + local task = tube:take() + test:is(task[1], 0, 'admin can take record') + tube:release(task[1]) + + -- Without grants. + box.session.su(test_user) + local stat = pcall(tube.take, tube) + test:is(stat, false, 'no access without grants') + box.session.su('admin') + + -- With role grants. + tube:grant_role(test_role) + + box.session.su(test_user) + local a = tube:take() + test:is(a[1], 0, 'take works with role grants') + local b = tube:take(0.1) + test:isnil(b, 'take timeout works with role grants') + local c = tube:ack(a[1]) + test:is(c[1], 0, 'ack works with role grants') + box.session.su('admin') + + tube:drop() + box.schema.user.drop(test_user) + box.schema.role.drop(test_role) +end) + test:test('check for call grants', function(test) -- prepare for tests rawset(_G, 'queue', require('queue')) @@ -203,6 +273,71 @@ test:test('check for call grants', function(test) rawset(_G, 'queue', nil) tube:drop() + box.schema.user.drop(test_user) +end) + +test:test('check for call grants via role', function(test) + rawset(_G, 'queue', require('queue')) + box.schema.user.create(test_user, { password = test_pass }) + box.schema.role.create(test_role) + box.schema.user.grant(test_user, test_role, nil, nil, { if_not_exists = true }) + + test:plan(9) + + local tube = queue.create_tube('test', 'fifo', { engine = engine }) + tube:put('help') + local task = tube:take() + test:is(task[1], 0, 'admin can take record') + tube:release(task[1]) + + -- Without call grants. + local nb_connect = netbox.connect or netbox.new + local con = nb_connect(('%s:%s@%s:%s'):format(test_user, test_pass, test_host, test_port)) + + local stat = pcall(con.call, con, 'queue.tube.test:take') + test:is(stat, false, 'no execute on tube function without call grants') + + -- Grant call via role. + tube:grant_role(test_role, { call = true }) + + local id = con:call('queue.identify') + test:ok(id ~= nil, 'queue.identify via net.box works with role call grants') + + local qc_arg_unpack = function(arg) + if qc.check_version({1, 7}) then + return arg + end + return arg and arg[1] + end + + local a = con:call('queue.tube.test:take') + test:is(qc_arg_unpack(a[1]), 0, 'take via net.box works with role call grants') + + local b = con:call('queue.tube.test:take', qc.pack_args(0.1)) + test:isnil(qc_arg_unpack(qc_arg_unpack(b)), 'take timeout via net.box works') + + local c = con:call('queue.tube.test:ack', qc.pack_args(qc_arg_unpack(a[1]))) + test:is(qc_arg_unpack(c[1]), 0, 'ack via net.box works') + + local d = con:call('queue.tube.test:put', {'help'}) + test:is(qc_arg_unpack(d[1]) >= 0, true, 'put via net.box works') + + local e = con:call('queue.statistics') + test:is(type(qc_arg_unpack(e)), 'table', 'queue.statistics via net.box works') + + tube:grant_role(test_role, { truncate = true }) + local f = con:call('queue.tube.test:truncate') + test:isnil(qc_arg_unpack(f), 'truncate via net.box works') + + -- Check double grants. + tube:grant_role(test_role, { call = true }) + + con:close() + rawset(_G, 'queue', nil) + + tube:drop() + box.schema.user.drop(test_user) + box.schema.role.drop(test_role) end) test:test('check tube existence', function(test) diff --git a/t/240-grant-via-role.t b/t/240-grant-via-role.t new file mode 100755 index 00000000..532f910a --- /dev/null +++ b/t/240-grant-via-role.t @@ -0,0 +1,315 @@ +#!/usr/bin/env tarantool +local test = require('tap').test() +test:plan(6) + +local test_pass = '1234' +local test_host = 'localhost' +local test_port = '3388' + +local netbox = require('net.box') +local tnt = require('t.tnt') +local qc = require('queue.compat') + +tnt.cfg{ + listen = ('%s:%s'):format(test_host, test_port), +} + +local engine = os.getenv('ENGINE') or 'memtx' + +test:test('grant_role does not affect direct user grant', function(test) + local queue = require('queue') + + local role = 'test_role_mix' + local user = 'test_user_mix' + + box.schema.role.create(role) + box.schema.user.create(user, { password = test_pass }) + box.schema.user.grant(user, role, nil, nil, { if_not_exists = true }) + + test:plan(4) + + local tube = queue.create_tube('test_mix_grants', 'fifo', { engine = engine }) + tube:put('a') + + -- Only role grants. + tube:grant_role(role) + + box.session.su(user) + local t = tube:take() + test:ok(t ~= nil, 'user can take via role') + tube:ack(t[1]) + box.session.su('admin') + + -- Add direct user grant and ensure still works. + tube:put('b') + tube:grant(user) + + box.session.su(user) + local t2 = tube:take() + test:ok(t2 ~= nil, 'user can take after direct user grant added') + tube:ack(t2[1]) + box.session.su('admin') + + -- Grant again should not break anything. + tube:grant_role(role) + tube:grant(user) + + tube:put('c') + box.session.su(user) + local t3 = tube:take() + test:ok(t3 ~= nil, 'user can take after repeated grants') + local a3 = tube:ack(t3[1]) + test:ok(a3 ~= nil, 'user can ack after repeated grants') + box.session.su('admin') + + tube:drop() + box.schema.user.drop(user) + box.schema.role.drop(role) +end) + +test:test('revoking role does not remove direct user grants', function(test) + local queue = require('queue') + + local role = 'test_role_revoke' + local user = 'test_user_revoke' + + box.schema.role.create(role) + box.schema.user.create(user, { password = test_pass }) + box.schema.user.grant(user, role, nil, nil, { if_not_exists = true }) + + test:plan(4) + + local tube = queue.create_tube('test_revoke_mix', 'fifo', { engine = engine }) + tube:put('a') + + -- Give both grants. + tube:grant_role(role) + tube:grant(user) + + -- Revoke role from user. + box.schema.user.revoke(user, role, nil, nil, { if_not_exists = true }) + + -- User should still have access due to direct grant. + box.session.su(user) + local t = tube:take() + test:ok(t ~= nil, 'user can take after role revoked (direct grant remains)') + local a = tube:ack(t[1]) + test:ok(a ~= nil, 'user can ack after role revoked') + box.session.su('admin') + + -- Revoke direct grants. + tube:put('b') + box.session.su(user) + local t2 = tube:take() + test:ok(t2 ~= nil, 'still can take (direct grant still present)') + tube:ack(t2[1]) + box.session.su('admin') + + test:ok(true, 'revoke role test completed') + + tube:drop() + box.schema.user.drop(user) + box.schema.role.drop(role) +end) + +test:test('grant_role: multiple users inherit access from one role', function(test) + rawset(_G, 'queue', require('queue')) + local queue = _G.queue + + local role = 'test_role' + local u1 = 'test_u1' + local u2 = 'test_u2' + local pass = test_pass + + box.schema.role.create(role) + box.schema.user.create(u1, { password = pass }) + box.schema.user.create(u2, { password = pass }) + + box.schema.user.grant(u1, role, nil, nil, { if_not_exists = true }) + box.schema.user.grant(u2, role, nil, nil, { if_not_exists = true }) + + test:plan(7) + + local tube = queue.create_tube('test_multi_role', 'fifo', { engine = engine }) + tube:put('a') + tube:put('b') + + tube:grant_role(role, { call = true }) + + local c1 = netbox.connect(('%s:%s'):format(test_host, test_port), { + user = u1, password = pass + }) + local c2 = netbox.connect(('%s:%s'):format(test_host, test_port), { + user = u2, password = pass + }) + + -- First user takes and acks first task. + local t1 = c1:call('queue.tube.test_multi_role:take') + test:is(t1[1], 0, 'u1 can take') + local a1 = c1:call('queue.tube.test_multi_role:ack', {t1[1]}) + test:is(a1[1], 0, 'u1 can ack') + + -- Second user takes and acks second task. + local t2 = c2:call('queue.tube.test_multi_role:take') + test:is(t2[1], 1, 'u2 can take next task') + local a2 = c2:call('queue.tube.test_multi_role:ack', {t2[1]}) + test:is(a2[1], 1, 'u2 can ack') + + -- First user puts from and takes again. + local p = c1:call('queue.tube.test_multi_role:put', {'c'}) + test:is(p[3], 'c', 'u1 can put') + + local t3 = c1:call('queue.tube.test_multi_role:take') + test:is(t3[3], 'c', 'u1 can take again after new put') + local a3 = c1:call('queue.tube.test_multi_role:ack', {t3[1]}) + test:is(a3[3], 'c', 'u1 can ack again') + + c1:close() + c2:close() + rawset(_G, 'queue', nil) + + tube:drop() + box.schema.user.drop(u1) + box.schema.user.drop(u2) + box.schema.role.drop(role) +end) + +test:test('grant_role: one role can access multiple tubes', function(test) + local queue = require('queue') + + local role = 'test_role' + local user = 'test_user' + + box.schema.role.create(role) + box.schema.user.create(user, { password = test_pass }) + box.schema.user.grant(user, role, nil, nil, { if_not_exists = true }) + + test:plan(4) + + local t1 = queue.create_tube('test_role_t1', 'fifo', { engine = engine }) + local t2 = queue.create_tube('test_role_t2', 'fifo', { engine = engine }) + + t1:put('x') + t2:put('y') + + t1:grant_role(role) + t2:grant_role(role) + + box.session.su(user, function() + local a = t1:take() + test:is(a[3], 'x', 'user can take from tube1') + t1:ack(a[1]) + + local b = t2:take() + test:is(b[3], 'y', 'user can take from tube2') + t2:ack(b[1]) + + test:ok(t1:put('z') ~= nil, 'user can put to tube1') + test:ok(t2:put('w') ~= nil, 'user can put to tube2') + end) + + t1:drop() + t2:drop() + box.schema.user.drop(user) + box.schema.role.drop(role) +end) + +test:test('grant_role call: multiple net.box users can call tube functions', function(test) + rawset(_G, 'queue', require('queue')) + local queue = _G.queue + + local role = 'test_role_call_multi' + local u1 = 'test_call_u1' + local u2 = 'test_call_u2' + + box.schema.role.create(role) + box.schema.user.create(u1, { password = test_pass }) + box.schema.user.create(u2, { password = test_pass }) + box.schema.user.grant(u1, role, nil, nil, { if_not_exists = true }) + box.schema.user.grant(u2, role, nil, nil, { if_not_exists = true }) + + test:plan(4) + + local tube = queue.create_tube('test_call_multi', 'fifo', { engine = engine }) + tube:put('a') + tube:put('b') + + tube:grant_role(role, { call = true }) + + local nb_connect = netbox.connect or netbox.new + local c1 = nb_connect(('%s:%s@%s:%s'):format(u1, test_pass, test_host, test_port)) + local c2 = nb_connect(('%s:%s@%s:%s'):format(u2, test_pass, test_host, test_port)) + + local t1 = c1:call('queue.tube.test_call_multi:take') + test:is(t1[1], 0, 'u1 can call take') + local a1 = c1:call('queue.tube.test_call_multi:ack', qc.pack_args(t1[1])) + test:is(a1[1], 0, 'u1 can call ack') + + local t2 = c2:call('queue.tube.test_call_multi:take') + test:is(t2[1], 1, 'u2 can call take') + local a2 = c2:call('queue.tube.test_call_multi:ack', qc.pack_args(t2[1])) + test:is(a2[1], 1, 'u2 can call ack') + + c1:close() + c2:close() + rawset(_G, 'queue', nil) + + tube:drop() + box.schema.user.drop(u1) + box.schema.user.drop(u2) + box.schema.role.drop(role) +end) + +test:test('grant_role: utube ready_buffer works for multiple users', function(test) + local queue = require('queue') + + local role = 'test_role_utube_rb' + local u1 = 'test_utube_u1' + local u2 = 'test_utube_u2' + + box.schema.role.create(role) + box.schema.user.create(u1, { password = test_pass }) + box.schema.user.create(u2, { password = test_pass }) + box.schema.user.grant(u1, role, nil, nil, { if_not_exists = true }) + box.schema.user.grant(u2, role, nil, nil, { if_not_exists = true }) + + test:plan(4) + + local tube = queue.create_tube('test_utube_rb', 'utube', { + engine = 'memtx', + storage_mode = 'ready_buffer', + }) + + tube:grant_role(role) + + tube:put('d1', { utube = 'k1' }) + tube:put('d2', { utube = 'k2' }) + + box.session.su(u1, function() + local t = tube:take() + test:ok(t ~= nil, 'u1 can take') + tube:ack(t[1]) + end) + + box.session.su(u2, function() + local t = tube:take() + test:ok(t ~= nil, 'u2 can take') + tube:ack(t[1]) + end) + + box.session.su(u1, function() + test:isnil(tube:take(0.01), 'no more tasks for u1') + end) + box.session.su(u2, function() + test:isnil(tube:take(0.01), 'no more tasks for u2') + end) + + tube:drop() + box.schema.user.drop(u1) + box.schema.user.drop(u2) + box.schema.role.drop(role) +end) + +tnt.finish() +os.exit(test:check() and 0 or 1) +-- vim: set ft=lua :