diff options
Diffstat (limited to 'lib/gitano')
-rw-r--r-- | lib/gitano/auth.lua | 140 | ||||
-rw-r--r-- | lib/gitano/config.lua | 139 | ||||
-rw-r--r-- | lib/gitano/log.lua | 57 | ||||
-rw-r--r-- | lib/gitano/repository.lua | 9 | ||||
-rw-r--r-- | lib/gitano/usercommand.lua | 168 | ||||
-rw-r--r-- | lib/gitano/util.lua | 14 |
6 files changed, 427 insertions, 100 deletions
diff --git a/lib/gitano/auth.lua b/lib/gitano/auth.lua new file mode 100644 index 0000000..8cdd8ec --- /dev/null +++ b/lib/gitano/auth.lua @@ -0,0 +1,140 @@ +local config = require 'gitano.config' +local command = require 'gitano.command' +local log = require 'gitano.log' +local repository = require 'gitano.repository' +local util = require 'gitano.util' +local gall = require 'gall' + +local function load_admin_conf(repo_root) + local admin_repo = gall.repository.new((repo_root or "") .. + "/gitano-admin.git") + + if not admin_repo then + log.critical("Unable to locate administration repository.") + return nil + end + + local admin_head = admin_repo:get(admin_repo.HEAD) + + if not admin_head then + log.critical("Unable to find the HEAD of the administration repository.") + return nil + end + + local admin_conf, msg = config.parse(admin_head) + + if not admin_conf then + log.critical("Unable to parse administration repository.") + log.critical(" * " .. (msg or "No error?")) + return nil + end + + return admin_conf +end + +local function set_environment(repo_root, repo, context, transactionid) + local env = { + ["GITANO_ROOT"] = repo_root, + ["GITANO_USER"] = context.user, + ["GITANO_KEYTAG"] = context.keytag, + ["GITANO_PROJECT"] = (repo or {}).name or "", + ["GITANO_SOURCE"] = context.source, + ["GITANO_TRANSACTION_ID"] = transactionid, + } + + for k, v in pairs(env) do + luxio.setenv(k, v) + end + + return env +end + +local function is_authorized(user, source, cmdline) + local repo_root = os.getenv("GITANO_ROOT") + local keytag = "" + local authorized = false + + local start_log_level = log.get_level() + log.cap_level(log.level.INFO) + local transactionid = log.syslog.open() + + config.repo_path(repo_root) + + if not user or not cmdline then + return nil + end + + local parsed_cmdline, warnings = util.parse_cmdline(cmdline) + + if (#warnings > 0) then + log.error("Error parsing command"); + return nil + end + + local admin_conf = load_admin_conf(repo_root) + if admin_conf == nil then + log.fatal("Couldn't load a config from the admin repository") + end + + if admin_conf.groups["gitano-admin"].filtered_members[user] then + log.set_level(start_log_level) + end + + if not admin_conf.global.silent then + log.bump_level(log.level.CHAT) + end + + ip = os.getenv("REMOTE_ADDR") or "unknown ip" + log.syslog.info("Client connected from", ip, "as", user, + "(" .. keytag .. ")", "Executing command:", cmdline) + + local cmd = command.get(parsed_cmdline[1]) + + if not cmd then + log.critical("Unknown command: " .. parsed_cmdline[1]) + return nil + end + + local repo + if cmd.takes_repo and #parsed_cmdline > 1 then + -- Acquire the repository object for the target repo + local msg + repo, msg = repository.find(admin_conf, parsed_cmdline[2]) + + if not repo then + log.critical("Unable to locate repository.") + log.critical(" * " .. (tostring(msg) or "No error")) + return nil + end + end + + if not cmd.validate(admin_conf, repo, parsed_cmdline) then + log.critical("Validation of command line failed") + return nil + end + + local context = {source = source, user = user, keytag = keytag} + local action, reason = cmd.prep(admin_conf, repo, parsed_cmdline, context) + + if not action then + log.critical(reason) + log.critical("Ruleset did not complete cleanly") + return nil + end + + local env + if action == "allow" then + log.info(reason or "Ruleset permitted action") + authorized = true + env = set_environment(repo_root, repo, context, transactionid) + else + log.critical(reason) + log.critical("Ruleset denied action. Sorry.") + end + + return authorized, cmd, parsed_cmdline, admin_conf, env +end + +return { + is_authorized = is_authorized +} diff --git a/lib/gitano/config.lua b/lib/gitano/config.lua index 865e222..afa9072 100644 --- a/lib/gitano/config.lua +++ b/lib/gitano/config.lua @@ -82,32 +82,31 @@ local function parse_admin_config(commit) for filename, obj in pairs(flat_tree) do local prefix, username = filename:match("^(users/.-)([a-z][a-z0-9_-]+)/user%.conf$") if prefix and username then - if not is_blob(obj) then - return nil, prefix .. username .. "/user.conf is not a blob?" - end - if users[username] then - return nil, "Duplicate user name: " .. username - end - -- Found a user, fill them out - local user_clod, err = - clod.parse(obj.obj.content, - commit.sha .. ":" .. prefix .. username .. "/user.conf") - - if not user_clod then - return nil, err - end - - if type(user_clod.settings.real_name) ~= "string" then - return nil, "gitano-admin:" .. commit.sha .. ":" .. prefix .. username .. "/user.conf missing real_name" - end - if (user_clod.settings.email_address and - type(user_clod.settings.email_address) ~= "string") then - return nil, "gitano-admin:" .. commit.sha .. ":" .. prefix .. username .. "/user.conf email_address is bad" - end - users[username] = setmetatable({ clod = user_clod, - keys = {}, - meta = { prefix = prefix }, - }, user_mt) + if not is_blob(obj) then + return nil, prefix .. username .. "/user.conf is not a blob?" + end + if users[username] then + return nil, "Duplicate user name: " .. username + end + + -- Found a user, fill them out + local user_clod, err = clod.parse(obj.obj.content, + commit.sha .. ":" .. prefix .. username .. "/user.conf") + + if not user_clod then + return nil, err + end + + if type(user_clod.settings.real_name) ~= "string" then + return nil, "gitano-admin:" .. commit.sha .. ":" .. prefix .. username .. "/user.conf missing real_name" + end + if (user_clod.settings.email_address and + type(user_clod.settings.email_address) ~= "string") then + return nil, "gitano-admin:" .. commit.sha .. ":" .. prefix .. username .. "/user.conf email_address is bad" + end + + users[username] = setmetatable({ clod = user_clod, keys = {}, + meta = { prefix = prefix }, }, user_mt) end end @@ -116,41 +115,41 @@ local function parse_admin_config(commit) for filename, obj in pairs(flat_tree) do local prefix, username, keyname = filename:match("^(users/.-)([a-z][a-z0-9_-]+)/([a-z][a-z0-9_-]+)%.key$") if prefix and username and keyname then - if not users[username] then - return nil, "Found a key (" .. keyname .. ") for " .. username .. " which lacks a user.conf" - end - local this_key = obj.obj.content - - this_key = this_key:gsub("\n*$", "") - - if this_key:match("\n") then - return nil, "Key " .. filename .. " has newlines in it -- is it in the wrong format?" - end - - local keytype, keydata, keytag = this_key:match("^([^ ]+) ([^ ]+) ([^ ].*)$") - if not (keytype and keydata and keytag) then - return nil, "Unable to parse key, " .. filename .. " did not smell like an OpenSSH v2 key" - end - if (keytype ~= "ssh-rsa") and (keytype ~= "ssh-dss") and - (keytype ~= "ecdsa-sha2-nistp256") and - (keytype ~= "ecdsa-sha2-nistp384") and - (keytype ~= "ecdsa-sha2-nistp521") then - return nil, "Unknown key type " .. keytype .. " in " .. filename - end - - if all_keys[this_key] then - return nil, ("Duplicate key found at (" .. keyname .. - ") for " .. username .. ". Previously found as (" .. - all_keys[this_key].keyname .. ") for " .. - all_keys[this_key].username) - end - all_keys[this_key] = { keyname = keyname, username = username } - users[username].keys[keyname] = { - data = this_key, - keyname = keyname, - username = username, - keytag = keytag, - } + if not users[username] then + return nil, "Found a key (" .. keyname .. ") for " .. username .. " which lacks a user.conf" + end + local this_key = obj.obj.content + + this_key = this_key:gsub("\n*$", "") + + if this_key:match("\n") then + return nil, "Key " .. filename .. " has newlines in it -- is it in the wrong format?" + end + + local keytype, keydata, keytag = this_key:match("^([^ ]+) ([^ ]+) ([^ ].*)$") + if not (keytype and keydata and keytag) then + return nil, "Unable to parse key, " .. filename .. " did not smell like an OpenSSH v2 key" + end + if (keytype ~= "ssh-rsa") and (keytype ~= "ssh-dss") and + (keytype ~= "ecdsa-sha2-nistp256") and + (keytype ~= "ecdsa-sha2-nistp384") and + (keytype ~= "ecdsa-sha2-nistp521") then + return nil, "Unknown key type " .. keytype .. " in " .. filename + end + + if all_keys[this_key] then + return nil, ("Duplicate key found at (" .. keyname .. ") for " .. + username .. ". Previously found as (" .. + all_keys[this_key].keyname .. ") for " .. + all_keys[this_key].username) + end + all_keys[this_key] = { keyname = keyname, username = username } + users[username].keys[keyname] = { + data = this_key, + keyname = keyname, + username = username, + keytag = keytag, + } end end @@ -369,30 +368,32 @@ local function commit_config_changes(conf, desc, username) -- write out everything we have here, and then prepare -- and write out a commit. local newtree = {} + -- Shallow copy the tree ready for mods, skipping users and groups for k,v in pairs(conf.content) do if not (k:match("^users/") or - k:match("^groups/")) then - newtree[k] = v + k:match("^groups/")) then + newtree[k] = v end end + -- Write out the site.conf - local obj = conf.repo.git:hash_object("blob", - conf.clod:serialise(), - true) + local obj = conf.repo.git:hash_object("blob", conf.clod:serialise(), true) newtree["site.conf"] = conf.repo.git:get(obj) + -- Construct all the users and write them out. for u, utab in pairs(conf.users) do local str = utab.clod:serialise() local obj = conf.repo.git:hash_object("blob", str, true) newtree[utab.meta.prefix .. u .. "/user.conf"] = conf.repo.git:get(obj) + -- Now the keys for k, ktab in pairs(utab.keys) do - obj = conf.repo.git:hash_object("blob", ktab.data .. "\n", true) - newtree[utab.meta.prefix .. u .. "/" .. k .. ".key"] = - conf.repo.git:get(obj) + obj = conf.repo.git:hash_object("blob", ktab.data .. "\n", true) + newtree[utab.meta.prefix .. u .. "/" .. k .. ".key"] = conf.repo.git:get(obj) end end + -- Do the same for the groups for g, gtab in pairs(conf.groups) do obj = conf.repo.git:hash_object("blob", gtab.clod:serialise(), true) diff --git a/lib/gitano/log.lua b/lib/gitano/log.lua index f243b87..e1df00b 100644 --- a/lib/gitano/log.lua +++ b/lib/gitano/log.lua @@ -14,6 +14,7 @@ local prefix = "[gitano] " local transactionid = nil local stream = sio.stderr +local is_buffered = false local ERRS = 0 local WARN = 1 @@ -24,6 +25,40 @@ local DEEPDEBUG = 5 local level = ERRS +local LogBuf = {} +LogBuf.__index = LogBuf + +function LogBuf:new() + return setmetatable({strings = {}}, self) +end + +function LogBuf:write(s) + table.insert(self.strings, s) +end + +function LogBuf:get() + return table.concat(self.strings) +end + +local function is_buffered_output() + return is_buffered +end + +local function buffer_output() + if not is_buffered_output() then + stream = LogBuf:new() + is_buffered = true + end +end + +local function get_buffered_output() + if is_buffered_output() then + return stream:get() + else + return nil + end +end + local function syslog_write(priority, ...) local strs = {...} @@ -98,15 +133,26 @@ end local function stdout(...) local savedstream, savedprefix = stream, prefix - stream, prefix = sio.stdout, "" + + prefix = "" + if not is_buffered_output() then + stream = sio.stdout + end + state(...) stream, prefix = savedstream, savedprefix end local function fatal(...) - syslog_write(luxio.LOG_EMERG, ...) + syslog_write(luxio.LOG_CRIT, ...) AT(ERRS, "FATAL:", ...) - stream:close() + + if is_buffered_output() then + sio.stderr:write(get_buffered_output()) + else + stream:close() + end + luxio._exit(1) end @@ -244,5 +290,8 @@ return { info = syslog_info, debug = syslog_debug, close = syslog_close, - } + }, + buffer_output = buffer_output, + is_buffered_output = is_buffered_output, + get_buffered_output = get_buffered_output } diff --git a/lib/gitano/repository.lua b/lib/gitano/repository.lua index 5d4a194..a15fcbe 100644 --- a/lib/gitano/repository.lua +++ b/lib/gitano/repository.lua @@ -183,8 +183,13 @@ function repo_method:check_local_git_files() end if anonexport then log.ddebug("<" .. self.name .. ">: Anonymous read allowed") - local fh = sio.open(self:fs_path() .. "/git-daemon-export-ok", "wc") - fh:close() + local fh, errmsg = sio.open(self:fs_path() .. "/git-daemon-export-ok", "wc") + + if fh then + fh:close() + else + log.warn("Can't create git-daemon-export file:", errmsg) + end else log.ddebug("<" .. self.name .. ">: Anonymous read not allowed") luxio.unlink(self:fs_path() .. "/git-daemon-export-ok") diff --git a/lib/gitano/usercommand.lua b/lib/gitano/usercommand.lua index d28b203..3c8b467 100644 --- a/lib/gitano/usercommand.lua +++ b/lib/gitano/usercommand.lua @@ -10,6 +10,7 @@ local repository = require 'gitano.repository' local config = require 'gitano.config' local sio = require 'luxio.simple' +local subprocess = require 'luxio.subprocess' local builtin_whoami_short = "Find out how Gitano identifies you" @@ -20,15 +21,19 @@ Tells you who you are, what your email address is set to, what keys you have registered etc. ]] -local function builtin_whoami_validate(config, repo, cmdline) - -- whoami +local function validate_single_argcmd(cmdline, msg) if #cmdline > 1 then - log.error("usage: whoami") + log.error(msg) return false end + return true end +local function builtin_whoami_validate(_, _, cmdline) + return validate_single_argcmd(cmdline, "usage: whoami") +end + local function builtin_whoami_prep(config, repo, cmdline, context) context.operation = "whoami" return config.repo:run_lace(context) @@ -37,6 +42,11 @@ end local function builtin_whoami_run(config, repo, cmdline, env) local username = env["GITANO_USER"] local userdata = config.users[username] + + if not userdata then + return "I don't know who you are", 1 + end + log.stdout(" User name:", username) log.stdout(" Real name:", userdata.real_name or "Unknown") log.stdout("Email address:", userdata.email_address or "unknown@example.com") @@ -159,53 +169,158 @@ local function builtin_sshkey_run(conf, _, cmdline, env) local utab = conf.users[env.GITANO_USER] if cmdline[2] == "list" then if not next(utab.keys) then - log.warn("There are no SSH keys registered for", env.GITANO_USER .. ", sorry") + log.warn("There are no SSH keys registered for", env.GITANO_USER + .. ", sorry") else - local pfx = " SSH key:" - for tagname, keydata in pairs(utab.keys) do - local suffix = (env.GITANO_KEYTAG == tagname) and " [*]" or "" - log.state(pfx, tagname, "=>", keydata.keytag .. suffix) - pfx = " " - end + local pfx = " SSH key:" + for tagname, keydata in pairs(utab.keys) do + local suffix = (env.GITANO_KEYTAG == tagname) and " [*]" or "" + log.state(pfx, tagname, "=>", keydata.keytag .. suffix) + pfx = " " + end end elseif cmdline[2] == "add" then local sshkey = sio.stdin:read("*l") local keytype, keydata, keytag = sshkey:match("^([^ ]+) ([^ ]+) ([^ ].*)$") if not (keytype and keydata and keytag) then - log.error("Unable to parse key,", filename, - "did not smell like an OpenSSH v2 key") - return "exit", 1 + log.error("Unable to parse key,", filename, + "did not smell like an OpenSSH v2 key") + return "exit", 1 end + if (keytype ~= "ssh-rsa") and (keytype ~= "ssh-dss") and - (keytype ~= "ecdsa-sha2-nistp256") and - (keytype ~= "ecdsa-sha2-nistp384") and - (keytype ~= "ecdsa-sha2-nistp521") then - log.error("Unknown key type", keytype) - return "exit", 1 + (keytype ~= "ecdsa-sha2-nistp256") and + (keytype ~= "ecdsa-sha2-nistp384") and + (keytype ~= "ecdsa-sha2-nistp521") then + log.error("Unknown key type", keytype) + return "exit", 1 end + local keytab = { - data = sshkey, - keyname = cmdline[3], - username = env.GITANO_USER, - keytag = keytag, + data = sshkey, + keyname = cmdline[3], + username = env.GITANO_USER, + keytag = keytag, } + utab.keys[cmdline[3]] = keytab - elseif cmdline[2] == "del" then utab.keys[cmdline[3]] = nil end if cmdline[2] ~= "list" then -- Store the config back. + local action = (cmdline[2] == "add") and "Added" or "Deleted" action = action .. " " .. cmdline[3] .. " for " .. env.GITANO_USER local ok, msg = config.commit(conf, action, env.GITANO_USER) + + if not ok then + log.error(msg) + return "exit", 1 + end + end + + return "exit", 0 +end + +local builtin_passwd_short = "Set your password" + +local builtin_passwd_helptext = [[ +usage: passwd + +Sets your password, the password is read from stdin. + +If no password is provided your password is removed (if you have one). +]] + +local function builtin_passwd_validate(_, _, cmdline) + return validate_single_argcmd(cmdline, "usage: passwd") +end + +local function builtin_passwd_prep(conf, repo, cmdline, context) + context.operation = "passwd" + + local action, reason = conf.repo:run_lace(context) + if action == "deny" then + return reason + end + + return action, reason +end + +local function update_htpasswd(user, passwd) + local htpasswd_path = os.getenv("HOME") .. "/htpasswd" + local flags = io.open(htpasswd_path, "r") and "" or "-c" + local exit_code + + if passwd ~= '' then + local proc = subprocess.spawn_simple({ + "htpasswd", flags, htpasswd_path, user, + stdin = passwd .. '\n' .. passwd .. '\n', + stdout = subprocess.PIPE, + stderr = subprocess.PIPE + }) + + _, exit_code = proc:wait() + else + local proc = subprocess.spawn_simple({ + "htpasswd", "-D", htpasswd_path, user, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE + }) + + _, exit_code = proc:wait() + end + + return exit_code == 0 +end + +local function builtin_passwd_run(conf, _, cmdline, env) + local user = env.GITANO_USER + + local password = sio.stdin:read("*l") + local method, hash = util.hash_password(password) + + if conf.users[user].hash == nil and password == "" then + log.chat(string.format("Password for %s is not set and no password was" + .. " provided, no action taken.", user)) + return "exit", 0 + end + + if password ~= "" then + conf.users[user].method = method + conf.users[user].hash = hash + else + -- user's password will be removed + conf.users[user].method = nil + conf.users[user].hash = nil + end + + local ok, msg + + if conf.clod.settings["use_htpasswd"] == "yes" then + ok = update_htpasswd(user, password) + if not ok then - log.error(msg) - return "exit", 1 + log.error("Failed to update htpasswd file") + return "exit", 1 end end + local action = string.format("%s password for %s", + password ~= '' and "Update" or "Remove", user) + + ok, msg = config.commit(conf, action, user) + + if not ok then + log.error(msg) + return "exit", 1 + end + + log.chat(string.format("%s password for %s", + password ~= '' and "Updated" or "Removed", user)) + return "exit", 0 end @@ -216,6 +331,9 @@ local function register_commands(reg) assert(reg("sshkey", builtin_sshkey_short, builtin_sshkey_helptext, builtin_sshkey_validate, builtin_sshkey_prep, builtin_sshkey_run, false, false)) + assert(reg("passwd", builtin_passwd_short, builtin_passwd_helptext, + builtin_passwd_validate, builtin_passwd_prep, + builtin_passwd_run, false, false)) end return { diff --git a/lib/gitano/util.lua b/lib/gitano/util.lua index 51c5bc2..c2a53a7 100644 --- a/lib/gitano/util.lua +++ b/lib/gitano/util.lua @@ -9,9 +9,20 @@ local luxio = require 'luxio' local sio = require 'luxio.simple' local log = require 'gitano.log' +local scrypt = require 'scrypt' local tconcat = table.concat +local check_password = scrypt.verify_password + +local function hash_password(password) + -- For the moment we are using scrypt, + -- we may decide to use other hash functions in the future + -- so it's useful to provide some way to identify which hash + -- function was used + return 'scrypt', scrypt.hash_password(password, 2^14, 8, 1) +end + local function _deep_copy(t, memo) if not memo then memo = {} end if memo[t] then return memo[t] end @@ -480,4 +491,7 @@ return { process_expansion = process_expansion, set = set, + + hash_password = hash_password, + check_password = check_password, } |