-- Print commands to drop users and roles that are not managed by
-- the YAML configuration.
--
-- These are ones created from a Lua code using one of the
-- following functions:
--
--  | box.schema.user.create()
--  | box.schema.role.create()
--
-- If users/roles are managed using the YAML configuration, then
-- it is likely that all of them are supposed to be managed this
-- way. If a user/a role is removed from the YAML configuration,
-- the admin expectation is likely that it is deleted from the
-- database.
--
-- However, users/roles created from Lua are not deleted
-- automatically due to compatibility concerns. It is an
-- admin responsibility to run this script and ensure that
-- there are no stalled entries. See [1] for the motivating
-- scenario.
--
-- [1]: https://github.com/tarantool/tarantool/issues/11827
--
-- Note: Users/roles created from the YAML configuration by
-- tarantool versions less than 3.6 can't be distinguished from
-- ones created from Lua and considered as the Lua ones.
--
-- Note: A user/a role can be created from Lua and from the YAML
-- configuration both. In this case box.schema.*.drop() deletes
-- the entry created from Lua. The entry that comes from the YAML
-- configuration remains in the database (until deleted from the
-- configuration).
--
-- Usage:
--
-- dofile('/path/to/this/file.lua')

local DEFAULT_ORIGIN = ''
local CONFIG_ORIGIN = 'config'
local USER_OPTS_FIELD = 8

-- Return origins for user/role from the given tuple.
local function user_origins_from_tuple(tuple)
    if tuple[USER_OPTS_FIELD] == nil or
       tuple[USER_OPTS_FIELD].origins == nil then
        return {[DEFAULT_ORIGIN] = true}
    end
    return tuple[USER_OPTS_FIELD].origins
end

local function is_system(tuple)
    return tuple.id <= box.schema.SYSTEM_USER_ID_MAX or
        tuple.id == box.schema.SUPER_ROLE_ID
end

local function disown_command(tuple)
    return ('box.schema.%s.disown(%q)'):format(tuple.type, tuple.name)
end

local function drop_command(tuple)
    return ('box.schema.%s.drop(%q)'):format(tuple.type, tuple.name)
end

local config_entries = {}
local duplicate_entries = {}
local orphan_entries = {}

for _, t in box.space._user:pairs() do
    if not is_system(t) then
        local origins = user_origins_from_tuple(t)
        local comes_from_config = origins[CONFIG_ORIGIN]
        local comes_from_lua = origins[DEFAULT_ORIGIN]

        if comes_from_config then
            table.insert(config_entries, ('%s %q'):format(t.type, t.name))
        end

        if comes_from_lua and comes_from_config then
            table.insert(duplicate_entries, disown_command(t))
        end

        if comes_from_lua and not comes_from_config then
            table.insert(orphan_entries, drop_command(t))
        end
    end
end

local function gen_disown_f(user_or_role)
    assert(user_or_role == 'user' or user_or_role == 'role')

    local func_name = ('box.schema.%s.disown'):format(user_or_role)
    local function e(fmt, ...)
        error(('%s: %s'):format(func_name, fmt:format(...)), 0)
    end

    return function(name)
        if type(name) ~= 'string' then
            e('expected string, got %s', type(name))
        end

        local t = box.space._user.index.name:get({name})
        if t == nil then
            e('unable to find %s %q', user_or_role, name)
        end

        local origins = user_origins_from_tuple(t)
        local comes_from_config = origins[CONFIG_ORIGIN]
        local comes_from_lua = origins[DEFAULT_ORIGIN]

        if not comes_from_config then
            e('the %s %q is not owned by the YAML configuration', user_or_role,
                name)
        end

        if not comes_from_lua then
            e('the %s %q is not owned by the Lua code', user_or_role, name)
        end

        box.schema[user_or_role].drop(name)
    end
end

if #config_entries == 0 then
    return 'Found no users/roles created from the YAML configuration.',
        'Assuming that the users/roles are managed in the old-fashion way',
        '(from Lua).',
        'No further actions are expected.'
end

if #duplicate_entries == 0 and #orphan_entries == 0 then
    return 'Found no users/roles created from Lua. Everything is managed ' ..
        'using the YAML configuration. Fine.'
end

-- The output is a return value, because print() is not visible to
-- the caller in case of a remote console connection.
local output = {}

-- Shortcut function: output a string.
local function o(v)
    table.insert(output, v)
end

-- Output a list.
local function l(vs)
    for _, v in ipairs(vs) do
        o(v)
    end
end

o('Found users/roles that are managed using the YAML configuration')
o('')
l(config_entries)

if #duplicate_entries > 0 then
    -- Add these functions only if needed.
    box.schema.user.disown = gen_disown_f('user')
    box.schema.role.disown = gen_disown_f('role')

    o('')
    o('Some of them will be kept even if removed from the YAML configuration.')
    o('This is a security risk. Use the following commands to transfer the')
    o('ownership to the configuration.')
    o('')
    l(duplicate_entries)
end

if #orphan_entries > 0 then
    o('')
    o('The following users/roles are NOT managed by the YAML configuration.')
    o('It is recommended to add them to the configuration or remove (if they')
    o('are obsoleted). The following commands remove them.')
    o('')
    l(orphan_entries)
end

return output
