From 250185e625a6049ea04f65c2286113495a2e6a03 Mon Sep 17 00:00:00 2001 From: Gleb Kashkin <g.kashkin@tarantool.org> Date: Thu, 17 Aug 2023 14:59:40 +0000 Subject: [PATCH] config: upgrade password sync Before this patch, user password could be set or updated only for auth_type == 'chap-sha1'. Now password can be set, updated or removed for any auth_type. Note that the password is changed only if necessary to minimise db writes. Part of #8967 NO_DOC=tarantool/doc#3544 links the most actual schema, no need to update the issue. --- .../gh-8967-upgrade-password-sync.md | 7 ++ src/box/lua/config/applier/credentials.lua | 84 +++++++++++++++---- .../credentials_applier_test.lua | 51 +++++++++++ 3 files changed, 126 insertions(+), 16 deletions(-) create mode 100644 changelogs/unreleased/gh-8967-upgrade-password-sync.md diff --git a/changelogs/unreleased/gh-8967-upgrade-password-sync.md b/changelogs/unreleased/gh-8967-upgrade-password-sync.md new file mode 100644 index 0000000000..4f29644619 --- /dev/null +++ b/changelogs/unreleased/gh-8967-upgrade-password-sync.md @@ -0,0 +1,7 @@ +## feature/config + +* Implemented a full password support in the `config.credentials` schema, + including a password setting, updating and removal for the `chap-sha1` + auth type (supported by both Tarantool Community Edition and Tarantool + Enterprise Edition) and the `pap-sha256` (just for Enterprise Edition + where it is available) (gh-8967). diff --git a/src/box/lua/config/applier/credentials.lua b/src/box/lua/config/applier/credentials.lua index dae96c12fe..e9ce2832aa 100644 --- a/src/box/lua/config/applier/credentials.lua +++ b/src/box/lua/config/applier/credentials.lua @@ -1,4 +1,5 @@ local log = require('internal.config.utils.log') +local digest = require('digest') --[[ This intermediate representation is a formatted set of @@ -371,27 +372,77 @@ end local function set_password(user_name, password) if password == nil then - if user_name ~= 'guest' then - log.verbose('credentials.apply: remove password for user %q', - user_name) - -- TODO: Check for hashes and if absent remove the password. - end - else if user_name == 'guest' then - error('Setting a password for the guest user has no effect') + -- Guest can't have a password, so this is valid and there + -- is nothing to do. + return + end + + local auth_def = box.space._user.index.name:get({user_name})[5] + if next(auth_def) == nil then + -- No password is currently set, there is nothing to do. + log.verbose('credentials.apply: user %q already has no password', + user_name) + return end - -- TODO: Check if the password can be hashed in somewhere other then - -- 'chap-sha1' or if the select{user_name} may return table of - -- a different shape. - local stored_user_def = box.space._user.index.name:get({user_name}) - local stored_hash = stored_user_def[5]['chap-sha1'] - local given_hash = box.schema.user.password(password) - if given_hash == stored_hash then + + log.verbose('credentials.apply: remove password for user %q', user_name) + -- No password is set, so remove it for the user. + -- There is no handy function for it, so remove it directly in + -- system space _user. Users are described in the following way: + -- + -- - [32, 1, 'myusername', 'user', {'chap-sha1': '<password_hash>'}, + -- [], 1692279984] + -- + -- The command below overrides with empty map the fifth tuple field, + -- containing hash (with auth_type == 'chap-sha1' and hash with salt + -- with auth_type == 'pap-sha256'). + box.space._user.index.name:update(user_name, + {{'=', 5, setmetatable({}, {__serialize = 'map'})}}) + + return + end + + if user_name == 'guest' then + error('Setting a password for the guest user is not allowed') + end + + local auth_def = box.space._user.index.name:get({user_name})[5] + if next(auth_def) == nil then + -- No password is currently set for the user, just set a new one. + log.verbose('credentials.apply: set a password for user %q', user_name) + box.schema.user.passwd(user_name, password) + return + end + + local auth_type = auth_def['chap-sha1'] and 'chap-sha1' or 'pap-sha256' + + if auth_type == 'chap-sha1' then + local current_hash = auth_def['chap-sha1'] + + local new_hash = box.schema.user.password(password) + if new_hash == current_hash then + log.verbose('credentials.apply: a password is already set ' .. + 'for user %q', user_name) + else + log.verbose('credentials.apply: set a password for user %q', + user_name) + box.schema.user.passwd(user_name, password) + end + else + assert(auth_def['pap-sha256']) + local current_salt = auth_def['pap-sha256'][1] + local current_hash = auth_def['pap-sha256'][2] + + local new_hash = digest.sha256(current_salt .. password) + if new_hash == current_hash then log.verbose('credentials.apply: a password is already set ' .. - 'for user %q', user_name) + 'for user %q', user_name) else log.verbose('credentials.apply: set a password for user %q', - user_name) + user_name) + -- Note: passwd() generated new random salt, it will be different + -- from current_salt. box.schema.user.passwd(user_name, password) end end @@ -456,5 +507,6 @@ return { privileges_subtract = privileges_subtract, privileges_add_defaults = privileges_add_defaults, sync_privileges = sync_privileges, + set_password = set_password, }, } diff --git a/test/config-luatest/credentials_applier_test.lua b/test/config-luatest/credentials_applier_test.lua index a55d1c06b5..9990b5a98e 100644 --- a/test/config-luatest/credentials_applier_test.lua +++ b/test/config-luatest/credentials_applier_test.lua @@ -478,3 +478,54 @@ g.test_sync_privileges = function(g) child:close() end + +g.test_set_password = function(g) + + local auth_types = { + 'chap-sha1', + 'pap-sha256', + } + + for _, auth_type in ipairs(auth_types) do + if auth_type == 'pap-sha256' then + t.tarantool.skip_if_not_enterprise() + end + local child = it.new() + local dir = treegen.prepare_directory(g, {}, {}) + local socket = "unix/:./test_socket.iproto" + + child:roundtrip(("box.cfg{work_dir = %q, listen = %q, auth_type = %q}") + :format(dir, socket, auth_type)) + child:roundtrip("nb = require('net.box')") + + local name = "myuser" + child:roundtrip(("myuser = %q"):format(name)) + child:roundtrip("box.schema.user.create(myuser)") + + child:roundtrip("set_password = require('internal.config.applier." .. + "credentials')._internal.set_password") + + child:roundtrip("set_password(myuser, 'password1')") + + child:roundtrip(("nb.connect(%q, {user = myuser, " .. + "password = 'password1'}).state"):format(socket), + "active") + + child:roundtrip("set_password(myuser, 'password2')") + + child:roundtrip(("nb.connect(%q, {user = myuser, " .. + "password = 'password1'}).state"):format(socket), + "error") + + child:roundtrip(("nb.connect(%q, {user = myuser, " .. + "password = 'password2'}).state"):format(socket), + "active") + + child:roundtrip("set_password(myuser, nil)") + + child:roundtrip(("nb.connect(%q, {user = myuser, " .. + "password = 'password2'}).state"):format(socket), + "error") + child:close() + end +end -- GitLab