summaryrefslogtreecommitdiff
path: root/lib/gitano
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gitano')
-rw-r--r--lib/gitano/auth.lua140
-rw-r--r--lib/gitano/config.lua139
-rw-r--r--lib/gitano/log.lua57
-rw-r--r--lib/gitano/repository.lua9
-rw-r--r--lib/gitano/usercommand.lua168
-rw-r--r--lib/gitano/util.lua14
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,
}