-- gitano.admincommand -- -- Gitano admin-commands -- -- Copyright 2012 Daniel Silverstone local log = require 'gitano.log' local util = require 'gitano.util' local repository = require 'gitano.repository' local config = require 'gitano.config' local clod = require 'clod' local luxio = require 'luxio' local function cmdmod() return require 'gitano.command' end local builtin_as_short = "Become someone else" local builtin_as_helptext = [[ usage: as ... Runs the given command line as the given user. The only limitation is that you are not permitted to run 'as' as someone else. ]] local function builtin_as_validate(config, _, cmdline) -- as if #cmdline < 3 then log.error("usage: as ...") return false end if cmdline[3] == "as" then log.error("Cannot use 'as' to run 'as'") return false end -- Strip the cmdline local cmdline_copy = util.deep_copy(cmdline) table.remove(cmdline_copy,1) table.remove(cmdline_copy,1) cmdline.copy = cmdline_copy -- Attempt to locate the command local cmd = cmdmod().get(cmdline[3]) if not cmd then log.error("Unknown command <" .. cmdline[3] .. ">") return false end cmdline.cmd = cmd -- If the returned command needs a repo, find it (and save it for later) local repo if cmd.takes_repo and #cmdline > 3 then -- Acquire the repository object for the target repo local msg repo, msg = repository.find(config, cmdline[4]) if not repo then log.critical("Unable to locate repository.") log.critical(" * " .. (tostring(msg))) log.fatal("Cannot continue") end if repo.is_nascent then log.info("Repository " .. repo.name .. " is nascent") end cmdline.repo = repo end -- Finally, validate us return cmd.validate(config, cmdline.repo, cmdline.copy) end local function builtin_as_prep(conf, _, cmdline, context) -- The context contains the user we are right now. -- We need to acquire information about that, so ask the config local as = { user = context.user } config.populate_context(conf, as) for k, v in pairs(as) do context["as_" .. k] = v end context.user = cmdline[2] -- Okay, we're now ready to chain through to the called command local res, msg = cmdline.cmd.prep(conf, cmdline.repo, cmdline.copy, context) if res == "allow" then -- Check to see if the user exists if conf.users[cmdline[2]] == nil then res, msg = "deny", "User '" .. cmdline[2] .. "' does not exist." end end return res, msg end local function builtin_as_run(conf, _, cmdline, env) -- Override some of the environment env.GITANO_USER = cmdline[2] env.GITANO_KEYTAG = "<*>" env.GITANO_PROJECT = (cmdline.repo or {}).name -- And then simply chain through return cmdline.cmd.run(conf, cmdline.repo, cmdline.copy, env) end local builtin_user_short = "Manage users in Gitano" local builtin_user_helptext = [[ usage: user [list] user add user del [confirm token] user email user name user rename [confirm token] With no subcommand, or the subcommand 'list' the user command will show a list of all the users, along with their email addresses and real names. With the 'add' subcommand, you can add a new user to the system. With the 'del' subcommand, you can delete a user from the system. With the 'email' subcommand, you can change a user's email address. With the 'name' subcommand, you can change a user's real name. If you try and delete or rename a user, you will need to paste a confirmation token which will be supplied if you try and delete or rename the user without it. That token is reliant on the state of the admin repository. Any admin operations performed between the two delete or rename attempts will invalidate the token and you will have to retry. ]] local function builtin_user_validate(conf, _, cmdline) if #cmdline == 1 then cmdline[2] = "list" end if cmdline[2] ~= "list" and cmdline[2] ~= "add" and cmdline[2] ~= "del" and cmdline[2] ~= "rename" and cmdline[2] ~= "email" and cmdline[2] ~= "name" then log.error("user takes one of list, add, del, rename, email or name") return false end if cmdline[2] == "list" and #cmdline ~= 2 then log.error("user list takes no arguments") return false end if #cmdline > 2 then cmdline[3] = cmdline[3]:lower() end if cmdline[2] == "add" and #cmdline < 5 then log.error("user add takes a username, email address and real name") return false end if cmdline[2] == "del" and (#cmdline < 3 or #cmdline > 4) then log.error("user del takes a username [and a confirmation token]") return false end if cmdline[2] == "email" and #cmdline ~= 4 then log.error("user email takes a username and an email address") return false end if cmdline[2] == "name" and #cmdline < 4 then log.error("user name takes a username and a real name") return false end if cmdline[2] == "rename" and (#cmdline < 4 or #cmdline > 5) then log.error("user rename takes a username, a new username [and a token]") return false end return true end local function builtin_user_prep(conf, _, cmdline, context) context.operation = "user" .. cmdline[2] -- userlist useradd userdel context.targetuser = cmdline[3] return conf.repo:run_lace(context) end local function builtin_user_run(conf, _, cmdline, env) local reason = nil if cmdline[2] == "list" then -- Listing all the users local users = {} for k in pairs(conf.users) do users[#users+1] = k end table.sort(users) for _, username in ipairs(users) do log.stdout(username .. ":" .. conf.users[username].email_address .. ":" .. conf.users[username].real_name) end elseif cmdline[2] == "add" then if conf.users[cmdline[3]] then log.fatal("User", cmdline[3], "already exists") end local new_name = util.deep_copy(cmdline) table.remove(new_name, 1) table.remove(new_name, 1) table.remove(new_name, 1) table.remove(new_name, 1) new_name = table.concat(new_name, " ") local new_clod = clod.parse("") new_clod.settings.email_address = cmdline[4] new_clod.settings.real_name = new_name local utab = { clod = new_clod, meta = { prefix = "users/" }, keys = {} } conf.users[cmdline[3]] = utab reason = "Create user " .. cmdline[3] elseif cmdline[2] == "name" then -- Find the named user local utab = conf.users[cmdline[3]] if not utab then log.fatal("Could not find user:", cmdline[3]) end local new_name = util.deep_copy(cmdline) table.remove(new_name, 1) table.remove(new_name, 1) table.remove(new_name, 1) new_name = table.concat(new_name, " ") if utab.real_name ~= new_name then utab.real_name = new_name reason = "Change real name of " .. cmdline[3] end elseif cmdline[2] == "email" then -- Find the named user local utab = conf.users[cmdline[3]] if not utab then log.fatal("Could not find user:", cmdline[3]) end if utab.email_address ~= cmdline[4] then utab.email_address = cmdline[4] reason = "Change email address of " .. cmdline[3] end elseif cmdline[2] == "del" then local username = cmdline[3] local utab = conf.users[username] if not utab then log.fatal("Could not find user:", username) end local token = conf.repo:generate_confirmation("delete " .. username) if not cmdline[4] then log.state("In order to delete", username, "you must supply the following token:") log.state(token) elseif cmdline[4] ~= token then log.error("Tokens do not match. Did someone else do administrative actions?") else -- Iterate groups and remove the user from any group it is -- a direct member of for g, gtab in pairs(conf.groups) do if gtab.members[username] then table.remove(gtab.members, gtab.members[username]) gtab.members[username] = nil gtab.changed_tables() log.state("Removed", username, "from membership of", g) end end -- Now remove the user conf.users[username] = nil -- And explain what reason = "Delete user " .. username end elseif cmdline[2] == "rename" then local oldusername = cmdline[3] local newusername = cmdline[4] local utab = conf.users[oldusername] if not utab then log.fatal("Could not find user:", oldusername) end if conf.users[newusername] then log.fatal("New username already exists:", newusername) end local token = conf.repo:generate_confirmation("rename " .. oldusername .. " to " .. newusername) if not cmdline[5] then log.state("In order to rename", oldusername, "you must supply the following token:") log.state(token) elseif cmdline[5] ~= token then log.error("Tokens do not match. Did someone else do administrative actions?") else -- Iterate groups and rename the user in any group it is -- a direct member of for g, gtab in pairs(conf.groups) do if gtab.members[oldusername] then gtab.members[gtab.members[oldusername]] = newusername gtab.members[newusername] = gtab.members[oldusername] gtab.members[oldusername] = nil gtab.changed_tables() log.state("Renamed:", oldusername, "to", newusername, "in membership of group:", g) end end -- Now rename the user itself conf.users[newusername] = utab conf.users[oldusername] = nil -- And explain what reason = "Rename user " .. oldusername .. " to " .. newusername end end if reason then -- Need to try and make a config commit local ok, commit = config.commit(conf, reason, env.GITANO_USER) if not ok then log.fatal(commit) end log.state("Committed: " .. reason) end return "exit", 0 end local builtin_group_short = "Manage groups in Gitano" local builtin_group_helptext = [[ usage: group [list] group show group add group del [confirm token] group description group adduser group deluser [confirm token] group addgroup group delgroup [confirm token] With no subcommand, or the subcommand 'list' the user command will show a list of all the users, along with their descriptions Showing a group will display membership information Adding a user to a group adds the user to the direct membership list. Removing a user from a group removes them from the direct membership list only. If you add a group to a group, you are stating that everyone in the sub group is to be considered a member of this group also. Removing a group undoes this effect. To delete or rename a group, remove a user from a group or remove a group from a group requires a confirmation token which will be supplied to you if missing. ]] local function builtin_group_validate(conf, _, cmdline) if not cmdline[2] then cmdline[2] = "list" end local groupsubs = util.set {"list", "show", "add", "del", "rename", "adduser", "deluser", "addgroup", "delgroup", "description"} if not groupsubs[cmdline[2]] then log.error("Unknown sub command", cmdline[2], "for group") return false end if #cmdline > 2 then cmdline[3] = cmdline[3]:lower() end if cmdline[2] == "list" and #cmdline ~= 2 then log.error("List takes no arguments") return false end if cmdline[2] == "show" and #cmdline ~= 3 then log.error("Show takes a group name") return false end if cmdline[2] == "add" and #cmdline < 4 then log.error("Add takes a group name and a description") return false end if cmdline[2] == "del" and (#cmdline < 3 or #cmdline > 4) then log.error("Del takes a group name and a confirmation token") return false end if cmdline[2] == "rename" and (#cmdline < 4 or #cmdline > 5) then log.error("Rename takes a group name, a new group name [and a token]") return false end if cmdline[2] == "description" and #cmdline < 4 then log.error("Description takes a group name and a description") return false end if cmdline[2] == "adduser" and #cmdline ~= 4 then log.error("Adduser takes a group name and a username") return false end if cmdline[2] == "deluser" and (#cmdline < 4 or #cmdline > 5) then log.error("Deluser takes a group name, a username and a confirmation token") return false end if cmdline[2] == "addgroup" and #cmdline ~= 4 then log.error("Addgroup takes a group name and a second group name") return false end if cmdline[2] == "delgroup" and (#cmdline < 4 or #cmdline > 5) then log.error("Delgroup takes a group name, a second group name, and a confirmation token") return false end if ({adduser=true,addgroup=true,deluser=true,delgroup=true})[cmdline[2]] then cmdline[4] = cmdline[4]:lower() end return true end local function builtin_group_prep(conf, _, cmdline, context) context.operation = "group" .. cmdline[2] util.add_splitable(context, "targetgroup", cmdline[3], "-", "prefix", "suffix") if cmdline[2]:match("user") or cmdline[2]:match("group") then util.add_splitable(context, "member", cmdline[4], "-", "prefix", "suffix") end return conf.repo:run_lace(context) end local function builtin_group_run(conf, _, cmdline, env) local reason = nil if cmdline[2] == "list" then local groups = {} for g in pairs(conf.groups) do groups[#groups+1] = g end table.sort(groups) for _, g in ipairs(groups) do log.stdout(g .. ":" .. conf.groups[g].settings.description) end elseif cmdline[2] == "show" then local g, gtab = cmdline[3], conf.groups[cmdline[3]] if not gtab then log.fatal("Unknown group", g) end log.stdout(g .. ":" .. gtab.settings.description) for i, m in ipairs(gtab.members) do log.stdout(" => " .. m) end for i, gg in ipairs(gtab.subgroups) do log.stdout(" [] " .. gg) end elseif cmdline[2] == "add" then local g = cmdline[3] if conf.groups[g] then log.fatal("Group", g, "already exists") end local new_desc = util.deep_copy(cmdline) table.remove(new_desc, 1) table.remove(new_desc, 1) table.remove(new_desc, 1) new_desc = table.concat(new_desc, " ") local new_clod = clod.parse("") new_clod.settings.description = new_desc local gtab = { clod = new_clod, members = {}, subgroups = {}, meta = { prefix = "groups/" } } conf.groups[g] = gtab reason = "Create group " .. g elseif cmdline[2] == "description" then local g, gtab = cmdline[3], conf.groups[cmdline[3]] if not gtab then log.fatal("Unknown group", g) end local new_desc = util.deep_copy(cmdline) table.remove(new_desc, 1) table.remove(new_desc, 1) table.remove(new_desc, 1) new_desc = table.concat(new_desc, " ") if gtab.settings.description ~= new_desc then gtab.settings.description = new_desc reason = "Change group description of " .. g end elseif cmdline[2] == "del" then local g = cmdline[3] if not conf.groups[g] then log.fatal("Unknown group", g) end local token = conf.repo:generate_confirmation("delete group " .. g) if not cmdline[4] then log.state("In order to delete group", g, "you must supply the following token:") log.state(token) elseif cmdline[4] ~= token then log.fatal("Token does not match. Has someone else done administrative actions?") else for gg, gtab in pairs(conf.groups) do if gtab.subgroups[g] then table.remove(gtab.subgroups, gtab.subgroups[g]) gtab.subgroups[g] = nil gtab.changed_tables() log.state("Removed:", g, "from subgroup membership of", gg) end end conf.groups[g] = nil reason = "Delete group " .. g end elseif cmdline[2] == "rename" then local g = cmdline[3] if not conf.groups[g] then log.fatal("Unknown group", g) end local newg = cmdline[4] if conf.groups[newg] then log.fatal("New group", newg, "already exists.") end local token = conf.repo:generate_confirmation("rename group " .. g .. " to " .. newg) if not cmdline[5] then log.state("In order to rename group", g, "to", newg, "you must supply the following token:") log.state(token) elseif cmdline[5] ~= token then log.fatal("Token does not match. Has someone else done administrative actions?") else for gg, gtab in pairs(conf.groups) do if gtab.subgroups[g] then gtab.subgroups[gtab.subgroups[g]] = newg gtab.subgroups[newg] = gtab.subgroups[g] gtab.subgroups[g] = nil gtab.changed_tables() log.state("Renamed:", g, "to", newg, "in subgroup membership of", gg) end end conf.groups[newg] = conf.groups[g] conf.groups[g] = nil reason = "Delete group " .. g end elseif cmdline[2] == "adduser" then local g, gtab, u, utab = cmdline[3], conf.groups[cmdline[3]], cmdline[4], conf.users[cmdline[4]] if not gtab then log.fatal("Unknown group", g) end if not utab then log.fatal("Unknown user", u) end if not gtab.members[u] then gtab.members[#gtab.members+1] = u gtab.members[u] = #gtab.members gtab.changed_tables() reason = "Add " .. u .. " to " .. g else log.state("User", u, "already a member of", g) end elseif cmdline[2] == "deluser" then local g, gtab, u, utab = cmdline[3], conf.groups[cmdline[3]], cmdline[4], conf.users[cmdline[4]] if not gtab then log.fatal("Unknown group", g) end if not utab then log.fatal("Unknown user", u) end if not gtab.members[u] then log.fatal("User", u, "is not a member of", g) end local token = conf.repo:generate_confirmation("delete user " .. u .. " from group " .. g) if not cmdline[5] then log.state("To delete user", u, "from group", g, "you will need this token:") log.state(token) elseif cmdline[5] ~= token then log.fatal("Token does not match. Did someone else do administrative actions?") else table.remove(gtab.members, gtab.members[u]) gtab.members[u] = nil gtab.changed_tables() reason = "Remove " .. u .. " from " .. g end elseif cmdline[2] == "addgroup" then local g, gtab, g2, g2tab = cmdline[3], conf.groups[cmdline[3]], cmdline[4], conf.groups[cmdline[4]] if not gtab then log.fatal("Unknown group", g) end if not g2tab then log.fatal("Unknown group", g2) end if not gtab.subgroups[g2] then gtab.subgroups[#gtab.subgroups+1] = g2 gtab.subgroups[g2] = #gtab.subgroups gtab.changed_tables() reason = "Add group " .. g2 .. " to " .. g else log.state("Group", g2, "already a subgroup of", g) end elseif cmdline[2] == "delgroup" then local g, gtab, g2, g2tab = cmdline[3], conf.groups[cmdline[3]], cmdline[4], conf.groups[cmdline[4]] if not gtab then log.fatal("Unknown group", g) end if not g2tab then log.fatal("Unknown group", g2) end if not gtab.subgroups[g2] then log.fatal("Group", g2, "is not a subgroup of", g) end local token = conf.repo:generate_confirmation("delete group " .. g .. "from group " .. g2) if not cmdline[5] then log.state("To delete group", g2, "from group", g, "you will need this token:") log.state(token) elseif cmdline[5] ~= token then log.fatal("Token does not match. Did someone else do administrative actions?") else table.remove(gtab.subgroups, gtab.subgroups[g2]) gtab.members[g2] = nil gtab.changed_tables() reason = "Remove group " .. g2 .. " from " .. g end else log.fatal("Unknown sub command", cmdline[2]) end if reason then local ok, commit = config.commit(conf, reason, env.GITANO_USER) if not ok then log.fatal(commit) end log.state("Committed: " .. reason) end return "exit", 0 end local builtin_graveyard_short = "Manage the graveyard" local builtin_graveyard_helptext = [[ usage: graveyard [list] graveyard restore graveyard purge [] With no subcommand, or the subcommand 'list', the graveyard command will list all the entries in the graveyard. With the restore subcommand, a graveyard entry will be restored to the location provided. The caller will need create permissions in the given location, as well as the rights to read the graveyard. With the purge subcommand, a graveyard entry will be purged entirely. Note that once a graveyard entry is purged, it cannot be restored except if you have made an out-of-band backup. ]] local function builtin_graveyard_validate(conf, _, cmdline) if #cmdline == 1 then cmdline[2] = "list" end if cmdline[2] == "list" then if #cmdline == 2 then return true end log.error("usage: graveyard list") end if cmdline[2] == "restore" then if #cmdline == 4 then cmdline[3] = cmdline[3]:gsub("/", "") return true end log.error("usage: graveyard restore ") end if cmdline[2] == "purge" then if #cmdline < 4 then if cmdline[3] then cmdline[3] = cmdline[3]:gsub("/", "") end return true end log.error("usage: graveyard purge []") end log.error("Unable to parse graveyard commandline properly") return false end local function builtin_graveyard_prep(conf, _, cmdline, context) local context_copy = util.deep_copy(context) context.operation = "graveyard" .. cmdline[2] if cmdline[2] == "restore" then context.target = cmdline[4] end if cmdline[2] == "purge" then context.target = cmdline[3] or "all" end local action, reason = conf.repo:run_lace(context) if action == "allow" and cmdline[2] == "restore" then context_copy.operation = "createrepo" local repo = repository.find(conf, cmdline[4]) return repo:run_lace(context_copy) end return action, reason end local function builtin_graveyard_run(conf, _, cmdline, env) local graveyard_base = config.repo_path() .. "/.graveyard/" if cmdline[2] == "list" then -- List the contents of the graveyard. local dirp, err = luxio.opendir(graveyard_base) if not dirp then if err == luxio.ENOTDIR then log.error("Graveyard is not present, or empty") return "exit", 1 end log.error("Error opening graveyard: " .. luxio.strerror(err)) return "exit", 1 end local e, i repeat e, i = luxio.readdir(dirp) if e == 0 then if not i.d_name:find("^%.") then log.state(i.d_name) end end until not e dirp = nil -- Allow GC of DIR handle elseif cmdline[2] == "restore" then -- Restoring a repository needs the prefix dirs to be made local temp_repo, msg = repository.find(conf, cmdline[4]) if not temp_repo then log.error("Unable to proceed: " .. msg) return "exit", 1 end local repo_path = temp_repo:fs_path() local restore_src = graveyard_base .. cmdline[3] -- Prepare the path towards the repo... local ok, msg = util.mkdir_p(util.dirname(repo_path)) if not ok then log.error("Unable to proceed: " .. msg) return "exit", 1 end -- Restore the repository log.info("Trying to restore", cmdline[3], "as", cmdline[4]) log.ddebug(restore_src, "=>", repo_path) local e, errno = luxio.rename(restore_src, repo_path) if e ~= 0 then log.error("Restore failed. Could not rename: " .. luxio.strerror(errno)) return "exit", 1 end -- Re-find the repository local repo, msg = repository.find(conf, cmdline[4]) if not repo then log.error("Restore failed. Could not find restored repo: " .. msg) return "exit", 1 end elseif cmdline[2] == "purge" then local match = cmdline[3] local to_remove = {} local dirp, err = luxio.opendir(graveyard_base) if not dirp then if err == luxio.ENOTDIR then log.error("Graveyard is not present, or empty") return "exit", 1 end log.error("Error opening graveyard: " .. luxio.strerror(err)) return "exit", 1 end local e, i repeat e, i = luxio.readdir(dirp) if e == 0 then if not i.d_name:find("^%.") then if not match or (match == i.d_name) then to_remove[#to_remove+1] = i.d_name end end end until not e dirp = nil -- Allow GC of DIR handle if #to_remove == 0 then if match then log.error("Unable to find", match, "to remove") else log.error("Nothing in the graveyard to purge") end return "exit", 1 end for i = 1, #to_remove do log.state("Purging", to_remove[i], "from graveyard...") local ok, msg = util.rm_rf(graveyard_base .. to_remove[i]) if not ok then log.error("Unable to remove:", msg) end end end return "exit", 0 end local function register_commands(reg) assert(reg("as", builtin_as_short, builtin_as_helptext, builtin_as_validate, builtin_as_prep, builtin_as_run, false, false, true)) assert(reg("user", builtin_user_short, builtin_user_helptext, builtin_user_validate, builtin_user_prep, builtin_user_run, false, false, true)) assert(reg("group", builtin_group_short, builtin_group_helptext, builtin_group_validate, builtin_group_prep, builtin_group_run, false, false, true)) assert(reg("graveyard", builtin_graveyard_short, builtin_graveyard_helptext, builtin_graveyard_validate, builtin_graveyard_prep, builtin_graveyard_run, false, false, true)) end return { register = register_commands }