summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Ipsum <richard.ipsum@codethink.co.uk>2013-10-04 11:41:36 +0100
committerRichard Ipsum <richard.ipsum@codethink.co.uk>2013-10-04 11:41:36 +0100
commit3791ac2aab5cc13fba01c18857e7cb0d7636b2e1 (patch)
tree77d24ddbe809c887036ea19c197a2fac9baac8ca
parent8397ee2115198b022991e2841ec702040a4617ff (diff)
parent4cb991a303e8826da72244e0ccfb40fea376260d (diff)
downloadgitano-3791ac2aab5cc13fba01c18857e7cb0d7636b2e1.tar.gz
Merge branch 'master' into baserock/morph
-rw-r--r--Makefile30
-rw-r--r--bin/gitano-auth.in14
-rw-r--r--bin/gitano-post-receive-hook.in37
-rw-r--r--bin/gitano-pre-receive-hook.in12
-rw-r--r--bin/gitano-update-hook.in11
-rw-r--r--bin/gitano-update-ssh.in3
-rw-r--r--lib/gitano/log.lua62
-rw-r--r--testing/.gitignore1
-rw-r--r--testing/01-basics.yarn106
-rw-r--r--testing/02-commands-as.yarn72
-rw-r--r--testing/02-commands-config.yarn1
-rw-r--r--testing/02-commands-copy.yarn1
-rw-r--r--testing/02-commands-count-objects.yarn1
-rw-r--r--testing/02-commands-create.yarn1
-rw-r--r--testing/02-commands-destroy.yarn1
-rw-r--r--testing/02-commands-fsck.yarn65
-rw-r--r--testing/02-commands-gc.yarn69
-rw-r--r--testing/02-commands-graveyard.yarn1
-rw-r--r--testing/02-commands-group.yarn1
-rw-r--r--testing/02-commands-help.yarn1
-rw-r--r--testing/02-commands-ls.yarn38
-rw-r--r--testing/02-commands-readme.yarn1
-rw-r--r--testing/02-commands-rename.yarn1
-rw-r--r--testing/02-commands-set-description.yarn1
-rw-r--r--testing/02-commands-set-head.yarn1
-rw-r--r--testing/02-commands-set-owner.yarn1
-rw-r--r--testing/02-commands-sshkey.yarn1
-rw-r--r--testing/02-commands-user.yarn1
-rw-r--r--testing/02-commands-whoami.yarn1
-rw-r--r--testing/gitano-test-tool.in182
-rw-r--r--testing/library.yarn104
31 files changed, 811 insertions, 11 deletions
diff --git a/Makefile b/Makefile
index 8c49fc4..8c725ec 100644
--- a/Makefile
+++ b/Makefile
@@ -25,6 +25,17 @@ LIB_BINS := gitano-auth gitano-post-receive-hook gitano-update-hook \
BINS := gitano-setup
+TEST_BIN_NAMES := gitano-test-tool
+
+TESTS := 01-basics 02-commands-as 02-commands-config 02-commands-copy \
+ 02-commands-count-objects 02-commands-create 02-commands-destroy \
+ 02-commands-fsck 02-commands-gc 02-commands-graveyard \
+ 02-commands-group 02-commands-help 02-commands-ls 02-commands-readme \
+ 02-commands-rename 02-commands-set-description 02-commands-set-head \
+ 02-commands-set-owner 02-commands-sshkey 02-commands-user \
+ 02-commands-whoami
+
+
MODS := gitano \
\
gitano.util \
@@ -54,6 +65,13 @@ SRC_MOD_FILES := $(patsubst %,lib/%,$(MOD_FILES))
LOCAL_BINS := $(patsubst %,bin/%,$(BINS) $(LIB_BINS))
LIB_BIN_SRCS := $(patsubst %,bin/%.in,$(LIB_BINS))
+TEST_BINS := $(patsubst %,testing/%,$(TEST_BIN_NAMES))
+TEST_BIN_SRCS := $(patsubst %,%.in,$(TEST_BINS))
+
+YARN := yarn
+
+TESTS := $(patsubst %,testing/%.yarn,$(TESTS))
+
GEN_BIN := utils/install-lua-bin
RUN_GEN_BIN := $(LUA) $(GEN_BIN) $(LUA)
define GEN_LOCAL_BIN
@@ -88,6 +106,8 @@ local: $(LOCAL_BINS)
clean:
@echo "CLEAN: local binaries"
@$(RM) $(LOCAL_BINS)
+ @echo "CLEAN: test binaries"
+ @$(RM) $(TEST_BINS)
distclean: clean
@find . -name "*~" -delete
@@ -95,6 +115,9 @@ distclean: clean
bin/%: bin/%.in $(GEN_BIN)
$(call GEN_LOCAL_BIN,$<,$@)
+testing/%: testing/%.in $(GEN_BIN)
+ $(call GEN_LOCAL_BIN,$<,$@)
+
install: install-bins install-lib-bins install-mods install-skel install-man
install-man:
@@ -118,3 +141,10 @@ install-skel:
for SKELFILE in $(SKEL_FILES); do \
install -m 644 skel/$$SKELFILE $(SKEL_INST_PATH)/$$SKELFILE; \
done
+
+test: local $(TEST_BINS)
+ @$(YARN) --env GTT="$$(pwd)/testing/gitano-test-tool" \
+ testing/library.yarn $(TESTS)
+
+testing/%: testing/%.in $(GEN_BIN)
+ $(call GEN_LOCAL_BIN,$<,$@)
diff --git a/bin/gitano-auth.in b/bin/gitano-auth.in
index e99eb11..8a91ae3 100644
--- a/bin/gitano-auth.in
+++ b/bin/gitano-auth.in
@@ -25,6 +25,8 @@ gitano.config.repo_path(repo_root)
local cmdline = luxio.getenv "SSH_ORIGINAL_COMMAND" or ""
+local transactionid = gitano.log.syslog.open()
+
if cmdline:match("^[ \t\n]*$") then
gitano.log.fatal("No command provided, cannot continue")
end
@@ -89,6 +91,13 @@ local repo
-- Find the command
+
+ip = string.match(luxio.getenv "SSH_CLIENT", "^[^ ]+") or ""
+
+gitano.log.syslog.info("Client connected from", ip, "as", username,
+ "(" .. keytag .. ")", "Executing command:",
+ cmdline)
+
local cmd = gitano.command.get(parsed_cmdline[1])
if not cmd then
@@ -153,6 +162,7 @@ local env = {
["GITANO_KEYTAG"] = keytag,
["GITANO_PROJECT"] = (repo or {}).name,
["GITANO_SOURCE"] = "ssh",
+ ["GITANO_TRANSACTION_ID"] = transactionid,
}
local how, why = cmd.run(config, repo, parsed_cmdline, env)
@@ -161,6 +171,10 @@ if how ~= "exit" or why ~= 0 then
gitano.log.critical("Error running sub-process:",
("%s (%d)"):format(how, why))
gitano.log.fatal("Unable to continue")
+else
+ gitano.log.syslog.info(cmdline, "completed successfully")
end
+gitano.log.syslog.close()
+
return 0
diff --git a/bin/gitano-post-receive-hook.in b/bin/gitano-post-receive-hook.in
index b272b17..f495d89 100644
--- a/bin/gitano-post-receive-hook.in
+++ b/bin/gitano-post-receive-hook.in
@@ -23,6 +23,7 @@ local start_log_level = gitano.log.get_level()
-- Clamp level at info until we have checked if the caller
-- is an admin or not
gitano.log.cap_level(gitano.log.level.INFO)
+gitano.log.syslog.open()
local repo_root = luxio.getenv("GITANO_ROOT")
local username = luxio.getenv("GITANO_USER") or "gitano/anonymous"
@@ -90,27 +91,41 @@ end
-- that the updates (if any) will have been applied
if updates["refs/gitano/admin"] then
- gitano.log.chat("<" .. repo.name .. ">",
- "Any changes to admin ref have been applied.")
+ local msg = "<" .. repo.name .. ">" .. " Any changes to admin ref have been applied."
+
+ gitano.log.chat(msg)
+ gitano.log.syslog.info(msg)
end
local function report_repo(reponame, repo, msg)
if repo then
- gitano.log.chat("<" .. reponame .. ">",
- "Any changes to hooks etc have been applied")
+ local s = "<" .. reponame ..">" .. " Any changes to hooks etc have been applied"
+
+ gitano.log.chat(s)
+ gitano.log.syslog.info(s)
else
- gitano.log.crit("<" .. reponame ..">", "Unable to process:", msg)
+ gitano.log.crit("<" .. reponame .. ">", "Unable to process:", msg)
end
end
if repo.name == "gitano-admin" and updates[admin_repo.HEAD] then
-- Updating the 'master' of gitano-admin, let's iterate all the repositories
- gitano.log.chat("Scanning repositories to apply hook/rules updates...")
+
+ gitano.log.syslog.info("Updating gitano-admin")
+
+ local msg = "Scanning repositories to apply hook/rules updates..."
+ gitano.log.chat(msg)
+ gitano.log.syslog.info(msg)
+
local ok, msg = gitano.repository.foreach(config, report_repo)
if not ok then
gitano.log.crit(msg)
end
- gitano.log.chat("All repositories updated where possible.")
+
+ msg = "All repositories updated where possible."
+ gitano.log.chat(msg)
+ gitano.log.syslog.info(msg)
+
local proc = sp.spawn({
gitano.config.lib_bin_path() .. "/gitano-update-ssh",
gitano.config.repo_path()
@@ -140,7 +155,11 @@ end
if repo:uses_hook("post-receive") then
gitano.log.debug("Configuring for post-receive hook")
gitano.actions.set_supple_globals("post-receive")
- gitano.log.info("Running repository post-receive hook")
+
+ local msg = "Running repository post-receive hook"
+ gitano.log.info(msg)
+ gitano.log.syslog.info(msg)
+
local info = {
username = username,
keytag = keytag,
@@ -155,4 +174,6 @@ if repo:uses_hook("post-receive") then
gitano.log.info("Finished")
end
+gitano.log.syslog.close()
+
return 0
diff --git a/bin/gitano-pre-receive-hook.in b/bin/gitano-pre-receive-hook.in
index 6eae987..c25418b 100644
--- a/bin/gitano-pre-receive-hook.in
+++ b/bin/gitano-pre-receive-hook.in
@@ -23,6 +23,7 @@ local start_log_level = gitano.log.get_level()
-- Clamp level at info until we have checked if the caller
-- is an admin or not
gitano.log.cap_level(gitano.log.level.INFO)
+gitano.log.syslog.open()
local repo_root = luxio.getenv("GITANO_ROOT")
local username = luxio.getenv("GITANO_USER") or "gitano/anonymous"
@@ -74,7 +75,7 @@ if repo.is_nascent then
gitano.log.fatal("Repository " .. repo.name .. " is nascent")
end
--- pre-receive is can prevent updates. Its name is a bit misleading.
+-- pre-receive can prevent updates. Its name is a bit misleading.
-- pre-receive is called once all the objects have been pushed, but before the
-- individual update hooks are called. It gets the same input as post-receive
-- but can opt to reject the entire push. If you need to make decisions based
@@ -91,7 +92,12 @@ end
if repo:uses_hook("pre-receive") then
gitano.log.debug("Configuring for pre-receive hook")
gitano.actions.set_supple_globals("pre-receive")
- gitano.log.info("Running repository pre-receive hook")
+
+ local msg = "Running repository pre-receive hook"
+
+ gitano.log.info(msg)
+ gitano.log.syslog.info(msg)
+
local info = {
username = username,
keytag = keytag,
@@ -106,4 +112,6 @@ if repo:uses_hook("pre-receive") then
gitano.log.info("Finished")
end
+gitano.log.syslog.close()
+
return 0
diff --git a/bin/gitano-update-hook.in b/bin/gitano-update-hook.in
index e838244..e338ba2 100644
--- a/bin/gitano-update-hook.in
+++ b/bin/gitano-update-hook.in
@@ -25,6 +25,7 @@ local start_log_level = gitano.log.get_level()
-- Clamp level at info until we have checked if the caller
-- is an admin or not
gitano.log.cap_level(gitano.log.level.INFO)
+gitano.log.syslog.open()
local nullsha = ("0"):rep(40)
@@ -258,7 +259,11 @@ end
if repo:uses_hook("update") then
gitano.log.debug("Configuring for update hook")
gitano.actions.set_supple_globals("update")
- gitano.log.info("Running repository update hook")
+
+ local msg = "Running repository update hook"
+ gitano.log.info(msg)
+ gitano.syslog.info(msg)
+
local info = {
username = username,
keytag = keytag,
@@ -277,4 +282,8 @@ end
gitano.log.info("Allowing ref update of",
refname, "from", oldsha, "to", newsha)
+gitano.log.syslog.info("Allowing ref update of", refname)
+
+gitano.log.syslog.close()
+
return 0
diff --git a/bin/gitano-update-ssh.in b/bin/gitano-update-ssh.in
index ecf51a9..798296f 100644
--- a/bin/gitano-update-ssh.in
+++ b/bin/gitano-update-ssh.in
@@ -23,6 +23,7 @@ local repo_root = ...
gitano.config.repo_path(repo_root)
gitano.log.bump_level(gitano.log.level.CHAT)
+gitano.log.syslog.open()
-- Now load the administration data
@@ -49,4 +50,6 @@ end
gitano.config.writessh(config)
+gitano.log.syslog.close()
+
return 0
diff --git a/lib/gitano/log.lua b/lib/gitano/log.lua
index e0e5648..f243b87 100644
--- a/lib/gitano/log.lua
+++ b/lib/gitano/log.lua
@@ -6,10 +6,12 @@
local luxio = require "luxio"
local sio = require "luxio.simple"
+local os = require "os"
local concat = table.concat
local prefix = "[gitano] "
+local transactionid = nil
local stream = sio.stderr
@@ -22,6 +24,53 @@ local DEEPDEBUG = 5
local level = ERRS
+local function syslog_write(priority, ...)
+ local strs = {...}
+
+ for i = 1, #strs do
+ strs[i] = tostring(strs[i]) or "?"
+ end
+
+ luxio.syslog(priority, transactionid .. ": " .. concat(strs, " ") .. "\n")
+end
+
+local function syslog_open()
+ local ident = "gitano"
+ transactionid = luxio.getenv("GITANO_TRANSACTION_ID")
+
+ if not transactionid then
+ transactionid = tostring(luxio.getpid()) .. "." .. os.date("%H%M%S")
+ end
+
+ luxio.openlog(ident, 0, luxio.LOG_DAEMON)
+
+ return transactionid
+end
+
+local function syslog_close()
+ luxio.closelog()
+end
+
+local function syslog_error(...)
+ syslog_write(luxio.LOG_ERR, ...)
+end
+
+local function syslog_warning(...)
+ syslog_write(luxio.LOG_WARNING, ...)
+end
+
+local function syslog_notice(...)
+ syslog_write(luxio.LOG_NOTICE, ...)
+end
+
+local function syslog_info(...)
+ syslog_write(luxio.LOG_INFO, ...)
+end
+
+local function syslog_debug(...)
+ syslog_write(luxio.LOG_DEBUG, ...)
+end
+
local function set_prefix(new_prefix)
if not new_prefix then
prefix = ""
@@ -55,12 +104,14 @@ local function stdout(...)
end
local function fatal(...)
+ syslog_write(luxio.LOG_EMERG, ...)
AT(ERRS, "FATAL:", ...)
stream:close()
luxio._exit(1)
end
local function critical(...)
+ syslog_write(luxio.LOG_CRIT, ...)
return AT(ERRS, "CRIT:", ...)
end
@@ -183,4 +234,15 @@ return {
fatal = fatal,
stdout = stdout,
set_prefix = set_prefix,
+ syslog = {
+ open = syslog_open,
+ err = syslog_error,
+ error = syslog_error,
+ warn = syslog_warning,
+ warning = syslog_warning,
+ notice = syslog_notice,
+ info = syslog_info,
+ debug = syslog_debug,
+ close = syslog_close,
+ }
}
diff --git a/testing/.gitignore b/testing/.gitignore
new file mode 100644
index 0000000..bf632a4
--- /dev/null
+++ b/testing/.gitignore
@@ -0,0 +1 @@
+gitano-test-tool
diff --git a/testing/01-basics.yarn b/testing/01-basics.yarn
new file mode 100644
index 0000000..18f2e4f
--- /dev/null
+++ b/testing/01-basics.yarn
@@ -0,0 +1,106 @@
+<!-- -*- markdown -*- -->
+Basic tests for Gitano
+======================
+
+In these basic tests for Gitano we start life by creating a standard
+installation and verifying some of the very basics of Gitano's core
+functionality.
+
+Basic behaviour
+---------------
+
+In this scenario we verify that we can create a standard instance and then
+clone the `gitano-admin` repository. Once we've done that we also verify that
+we can create a user, give it an ssh key, that the user's creation is reflected
+in the `gitano-admin` repository and that we can remove the user and the
+removal is also reflected.
+
+ SCENARIO Verification of basic behaviour
+
+Step 1 is to create a standard instance and clone `gitano-admin`
+
+ GIVEN a standard instance
+ WHEN testinstance, using adminkey, clones gitano-admin as gitano-admin
+ THEN testinstance has a clone of gitano-admin
+
+Next we create the user (alice) and verify that `gitano-admin` shows her.
+
+ GIVEN a unix user called alice
+ AND alice has keys called main
+ WHEN testinstance, using adminkey, adds user alice, using alice main
+ AND git pull happens in testinstance gitano-admin
+ THEN testinstance gitano-admin has a file called users/alice/user.conf
+ AND testinstance gitano-admin has a file called users/alice/default.key
+
+Finally we remove that user and verify that `gitano-admin` reflects that too.
+
+ WHEN testinstance, using adminkey, deletes user alice
+ AND git pull happens in testinstance gitano-admin
+ THEN testinstance gitano-admin has no file called users/alice/user.conf
+ AND testinstance gitano-admin has no file called users/alice/default.key
+
+Users can see what groups they are in
+-------------------------------------
+
+In this scenario we take a standard instance and ensure that the `testinstance`
+user can access their user information (their `whoami` output) and that
+information lists the `gitano-admin` group which they should be a member of.
+
+ SCENARIO whoami shows the gitano-admin group
+
+ GIVEN a standard instance
+ WHEN testinstance adminkey runs whoami
+ THEN stdout contains gitano-admin
+
+Then, just to be sure, we create a new user and ensure that it does not have
+membership of `gitano-admin`
+
+ GIVEN a unix user called alice
+ AND alice has keys called main
+ WHEN testinstance, using adminkey, adds user alice, using alice main
+ AND alice main runs whoami
+ THEN stdout does not contain gitano-admin
+
+Non-admin users cannot see the `gitano-admin` repository
+--------------------------------------------------------
+
+In this scenario we take a standard instance, add a user to it, and verify that
+when the new user runs 'ls' it doesn't get to see `gitano-admin` but that the
+`testinstance` user gets to see it and has `RW` privs.
+
+ SCENARIO ls will not show repositories you have no access to
+
+ GIVEN a standard instance
+ AND a unix user called alice
+ AND alice has keys called main
+ WHEN testinstance, using adminkey, adds user alice, using alice main
+ AND alice main runs ls
+ THEN stdout does not contain gitano-admin
+ WHEN testinstance adminkey runs ls
+ THEN stdout contains RW gitano-admin
+
+Basic repository creation
+-------------------------
+
+In a default configuration, the only user who will be able to create
+repositories. However creation can hand off ownership which means that we can
+test that a new user who has a repository created for them can see it in ls.
+
+ SCENARIO delegated repository creation works
+
+ GIVEN a standard instance
+ AND a unix user called alice
+ AND alice has keys called main
+ WHEN testinstance, using adminkey, adds user alice, using alice main
+ AND testinstance adminkey runs create somerepo alice
+ AND alice main runs ls
+ THEN stdout contains RW somerepo
+
+And just to check, if the `testinstance` user created a non-delegated repo then
+the `alice` user cannot see it.
+
+ WHEN testinstance adminkey runs create anotherrepo
+ AND testinstance adminkey runs ls
+ THEN stdout contains RW anotherrepo
+ WHEN alice main runs ls
+ THEN stdout does not contain anotherrepo
diff --git a/testing/02-commands-as.yarn b/testing/02-commands-as.yarn
new file mode 100644
index 0000000..ce8afbf
--- /dev/null
+++ b/testing/02-commands-as.yarn
@@ -0,0 +1,72 @@
+<!-- -*- markdown -*- -->
+as --A- Become someone else
+===========================
+
+The `as` command can be used to run commands as different users. It should not
+leak the existence/absence of a user, nor should it leak permissions from the
+calling user into the effective user.
+
+Verification of `as` in the simple case
+---------------------------------------
+
+In the simple case, `as` is being called by someone who has permission to do
+so, on behalf of a user which exists and can be used.
+
+ SCENARIO Default case for as
+
+ GIVEN a standard instance
+ AND testinstance has keys called other
+ WHEN testinstance, using adminkey, adds user other, using testinstance other
+ AND testinstance adminkey runs as other whoami
+ THEN stdout contains other
+ AND stdout does not contain gitano-admin
+
+The other trivial case is that a user without permission tries to run `as`.
+
+ WHEN testinstance other, expecting failure, runs as other whoami
+ THEN stdout is empty
+ AND stderr contains Ruleset denied action
+ AND stderr contains exit:1
+
+The final trivial case is that a user which can run `as` cannot use it to run
+`as`.
+
+ WHEN testinstance adminkey, expecting failure, runs as other as other whoami
+ THEN stdout is empty
+ AND stderr contains Cannot use 'as' to run 'as'
+ AND stderr contains Validation of command line failed
+ AND stderr contains exit:1
+
+Security-related cases for `as` invocation
+------------------------------------------
+
+There are a number of security implications for the `as` command. In the
+simplest of cases it is only necessary to grant gitano-admin members the right
+to run commands `as` other users. In this way, only those who could otherwise
+alter the users in the first place can act on their behalf.
+
+There is, however, a potential information leak -- namely if someone who does
+not have the right to run commands 'as' another user runs an `as` with a user
+which does not exist. It is critical that this simply be reported as a lack of
+permission to run any command, and not leak that the target user does not exist
+in any way.
+
+ SCENARIO Ensuring 'as' does not leak user presence
+
+ GIVEN a standard instance
+ AND testinstance has keys called other
+ WHEN testinstance, using adminkey, adds user other, using testinstance other
+ AND testinstance adminkey runs as other whoami
+ THEN stderr is empty
+ WHEN testinstance other, expecting failure, runs as badger whoami
+ THEN stdout is empty
+ AND stderr does not contain badger
+
+Finally we ensure that when a user who may run `as` commands does so, but
+manages to typo a username, they get a useful error message.
+
+ WHEN testinstance adminkey, expecting failure, runs as badger whoami
+ THEN stderr contains badger
+ AND stderr contains does not exist
+ AND stderr contains exit:1
+ AND stdout is empty
diff --git a/testing/02-commands-config.yarn b/testing/02-commands-config.yarn
new file mode 100644
index 0000000..f282cb0
--- /dev/null
+++ b/testing/02-commands-config.yarn
@@ -0,0 +1 @@
+<!-- -*- markdown -*- -->
diff --git a/testing/02-commands-copy.yarn b/testing/02-commands-copy.yarn
new file mode 100644
index 0000000..f282cb0
--- /dev/null
+++ b/testing/02-commands-copy.yarn
@@ -0,0 +1 @@
+<!-- -*- markdown -*- -->
diff --git a/testing/02-commands-count-objects.yarn b/testing/02-commands-count-objects.yarn
new file mode 100644
index 0000000..f282cb0
--- /dev/null
+++ b/testing/02-commands-count-objects.yarn
@@ -0,0 +1 @@
+<!-- -*- markdown -*- -->
diff --git a/testing/02-commands-create.yarn b/testing/02-commands-create.yarn
new file mode 100644
index 0000000..f282cb0
--- /dev/null
+++ b/testing/02-commands-create.yarn
@@ -0,0 +1 @@
+<!-- -*- markdown -*- -->
diff --git a/testing/02-commands-destroy.yarn b/testing/02-commands-destroy.yarn
new file mode 100644
index 0000000..f282cb0
--- /dev/null
+++ b/testing/02-commands-destroy.yarn
@@ -0,0 +1 @@
+<!-- -*- markdown -*- -->
diff --git a/testing/02-commands-fsck.yarn b/testing/02-commands-fsck.yarn
new file mode 100644
index 0000000..115bbc6
--- /dev/null
+++ b/testing/02-commands-fsck.yarn
@@ -0,0 +1,65 @@
+<!-- -*- markdown -*- -->
+fsck ---- Perform a fsck operation on a repository (Takes a repo)
+=================================================================
+
+The `fsck` command is a basic pass-through to the underlying `git fsck` being
+run on the remote repository. Apart from ensuring that the caller has `write`
+access, to a repository which exists, no other checks are done and any spare
+arguments are passed through to `git fsck`.
+
+Simple `fsck` usage
+-------------------
+
+In the simple case a `gitano-admin` runs `fsck` against a repository which
+definitely exists and as they are `gitano-admin` they have write access.
+
+ SCENARIO Simple `fsck` cases
+
+ GIVEN a standard instance
+ WHEN testinstance adminkey runs fsck gitano-admin
+ THEN stdout is empty
+ AND stderr is empty
+
+No matter how powerful you are, you cannot `fsck` a repository which does not
+exist...
+
+ WHEN testinstance adminkey, expecting failure, runs fsck somethingelse
+ THEN stdout is empty
+ AND stderr contains repository does not exist
+
+Attempting to `fsck` when you have no write access
+--------------------------------------------------
+
+Since any non-`gitano-admin` member cannot see `gitano-admin` we can use that
+as a test case for ensuring that you must have write access in order to `fsck`
+something.
+
+ SCENARIO lowly accolyte fails to fsck
+
+ GIVEN a standard instance
+ AND testinstance has keys called other
+ WHEN testinstance, using adminkey, adds user other, using testinstance other
+ AND testinstance other, expecting failure, runs fsck gitano-admin
+ THEN stdout is empty
+ AND stderr contains Ruleset denied action
+
+Passing commands through to `fsck`
+----------------------------------
+
+It is possible to pass arguments through to the `git fsck` subprocess. By
+passing through a bad option, we get to see this in action
+
+ SCENARIO passing arguments to `fsck`
+
+ GIVEN a standard instance
+ WHEN testinstance adminkey, expecting failure, runs fsck gitano-admin --bad-option
+ THEN stdout is empty
+
+We check for `git fsck`'s usage message:
+
+ AND stderr contains error: unknown option
+ AND stderr contains usage: git fsck
+
+And also we see that Gitano has caught the error
+
+ AND stderr contains Unable to continue
diff --git a/testing/02-commands-gc.yarn b/testing/02-commands-gc.yarn
new file mode 100644
index 0000000..d1cff46
--- /dev/null
+++ b/testing/02-commands-gc.yarn
@@ -0,0 +1,69 @@
+<!-- -*- markdown -*- -->
+gc ---- Invoke git gc on your repository (Takes a repo)
+=======================================================
+
+The `gc` command is a basic pass-through to the underlying `git gc` being run
+on the remote repository. Apart from ensuring that the caller has `write`
+access, to a repository which exists, no other checks are done and any spare
+arguments are passed through to `git gc`.
+
+Using `gc` in the simple case
+-----------------------------
+
+So the simple case is that a `gitano-admin` runs `gc` on a repository which
+definitely exists which means they always have the rights to do so.
+
+ SCENARIO Simple case `gc` usage
+
+ GIVEN a standard instance
+ WHEN testinstance adminkey runs gc gitano-admin
+ THEN stderr is empty
+ AND stdout is empty
+
+We can then ensure that if the repository does not exist, we get a useful error
+message back:
+
+ SCENARIO Simple failure case `gc` usage
+
+ GIVEN a standard instance
+ WHEN testinstance adminkey, expecting failure, runs gc something
+ THEN stdout is empty
+ AND stderr contains repository does not exist
+
+Write access checks
+-------------------
+
+A more complex case involves creating a repository to which a user has no write
+permissions and trying to get that user to run `gc` on it.
+
+ SCENARIO Write access checks for `gc` usage
+
+ GIVEN a standard instance
+ AND testinstance has keys called other
+ WHEN testinstance, using adminkey, adds user other, using testinstance other
+ AND testinstance adminkey runs create testrepo
+ AND testinstance other, expecting failure, runs gc testrepo
+ THEN stdout is empty
+ AND stderr contains Ruleset denied action
+
+Passing arguments to `git gc`
+-----------------------------
+
+Any spare arguments given to `gc` are passed through to `git gc` untouched. We
+can verify that arguments are passed through by passing a bad argument through
+and seeing if we get an error message from the underlying `git gc` instance:
+
+ SCENARIO Passing arguments through to `git gc`
+
+ GIVEN a standard instance
+ WHEN testinstance adminkey, expecting failure, runs gc gitano-admin --not-valid
+ THEN stdout is empty
+
+These are the `git gc` errors
+
+ AND stderr contains error: unknown option
+ AND stderr contains usage: git gc
+
+And this demonstrates that Gitano detected the error properly
+
+ AND stderr contains Unable to continue
diff --git a/testing/02-commands-graveyard.yarn b/testing/02-commands-graveyard.yarn
new file mode 100644
index 0000000..f282cb0
--- /dev/null
+++ b/testing/02-commands-graveyard.yarn
@@ -0,0 +1 @@
+<!-- -*- markdown -*- -->
diff --git a/testing/02-commands-group.yarn b/testing/02-commands-group.yarn
new file mode 100644
index 0000000..f282cb0
--- /dev/null
+++ b/testing/02-commands-group.yarn
@@ -0,0 +1 @@
+<!-- -*- markdown -*- -->
diff --git a/testing/02-commands-help.yarn b/testing/02-commands-help.yarn
new file mode 100644
index 0000000..f282cb0
--- /dev/null
+++ b/testing/02-commands-help.yarn
@@ -0,0 +1 @@
+<!-- -*- markdown -*- -->
diff --git a/testing/02-commands-ls.yarn b/testing/02-commands-ls.yarn
new file mode 100644
index 0000000..6a6178c
--- /dev/null
+++ b/testing/02-commands-ls.yarn
@@ -0,0 +1,38 @@
+<!-- -*- markdown -*- -->
+
+`ls [--verbose|-v] [<pattern>...]`
+==================================
+
+The `ls` command is one of the few which touch every repository in a Gitano
+instance. As such, it can take a while to run. Theoretically it leaks the
+number of Git repositories on the server by virtue of analysis of timing.
+
+Basic operation
+===============
+
+Firstly, we verify the basic operation of ls, that as a gitano-admin we have
+read access (at least) to everything and as such we can list all the
+repositories.
+
+ SCENARIO Basic operation of ls
+ GIVEN a standard instance
+ WHEN testinstance adminkey runs ls
+ THEN stdout contains RW gitano-admin
+ AND stderr is empty
+
+General access control for ls
+=============================
+
+If you have no read or write access to a repository, it should not show up
+when you run `ls`.
+
+ SCENARIO No access means no show in ls
+ GIVEN a standard instance
+ AND testinstance has keys called other
+ WHEN testinstance, using adminkey, adds user other, using testinstance other
+ AND testinstance adminkey runs create stoat
+ AND testinstance other runs ls
+ THEN stdout does not contain stoat
+ AND stderr is empty
+
+TODO: Add more tests when we have rule control to govern things a little more.
diff --git a/testing/02-commands-readme.yarn b/testing/02-commands-readme.yarn
new file mode 100644
index 0000000..f282cb0
--- /dev/null
+++ b/testing/02-commands-readme.yarn
@@ -0,0 +1 @@
+<!-- -*- markdown -*- -->
diff --git a/testing/02-commands-rename.yarn b/testing/02-commands-rename.yarn
new file mode 100644
index 0000000..f282cb0
--- /dev/null
+++ b/testing/02-commands-rename.yarn
@@ -0,0 +1 @@
+<!-- -*- markdown -*- -->
diff --git a/testing/02-commands-set-description.yarn b/testing/02-commands-set-description.yarn
new file mode 100644
index 0000000..f282cb0
--- /dev/null
+++ b/testing/02-commands-set-description.yarn
@@ -0,0 +1 @@
+<!-- -*- markdown -*- -->
diff --git a/testing/02-commands-set-head.yarn b/testing/02-commands-set-head.yarn
new file mode 100644
index 0000000..f282cb0
--- /dev/null
+++ b/testing/02-commands-set-head.yarn
@@ -0,0 +1 @@
+<!-- -*- markdown -*- -->
diff --git a/testing/02-commands-set-owner.yarn b/testing/02-commands-set-owner.yarn
new file mode 100644
index 0000000..f282cb0
--- /dev/null
+++ b/testing/02-commands-set-owner.yarn
@@ -0,0 +1 @@
+<!-- -*- markdown -*- -->
diff --git a/testing/02-commands-sshkey.yarn b/testing/02-commands-sshkey.yarn
new file mode 100644
index 0000000..f282cb0
--- /dev/null
+++ b/testing/02-commands-sshkey.yarn
@@ -0,0 +1 @@
+<!-- -*- markdown -*- -->
diff --git a/testing/02-commands-user.yarn b/testing/02-commands-user.yarn
new file mode 100644
index 0000000..f282cb0
--- /dev/null
+++ b/testing/02-commands-user.yarn
@@ -0,0 +1 @@
+<!-- -*- markdown -*- -->
diff --git a/testing/02-commands-whoami.yarn b/testing/02-commands-whoami.yarn
new file mode 100644
index 0000000..f282cb0
--- /dev/null
+++ b/testing/02-commands-whoami.yarn
@@ -0,0 +1 @@
+<!-- -*- markdown -*- -->
diff --git a/testing/gitano-test-tool.in b/testing/gitano-test-tool.in
new file mode 100644
index 0000000..8436dd6
--- /dev/null
+++ b/testing/gitano-test-tool.in
@@ -0,0 +1,182 @@
+-- @@SHEBANG
+-- -*- lua -*-
+-- gitano-test-tool
+--
+-- Git (with) Augmented network operations -- testing tool
+--
+-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
+--
+--
+
+-- @@GITANO_LUA_PATH
+
+local gitano = require "gitano"
+local gall = require "gall"
+local luxio = require "luxio"
+local sio = require "luxio.simple"
+local sp = require "luxio.subprocess"
+
+-- @@GITANO_BIN_PATH
+-- @@GITANO_SHARE_PATH
+
+local argv = {...}
+local basedir = (luxio.getenv "DATADIR") .. "/"
+
+local function user_home(username)
+ return basedir .. "user-home-" .. username
+end
+
+local function ssh_base(username)
+ return user_home(username) .. "/.ssh"
+end
+
+local function ssh_key_file(username, keyname)
+ return ssh_base(username) .. "/" .. keyname
+end
+
+local function unix_assert(ret, errno)
+ if ret ~= 0 then
+ error(luxio.strerror(errno))
+ end
+end
+
+local function run_program(t)
+ local proc = sp.spawn_simple(t)
+ local how, why = proc:wait()
+ if how == -1 then
+ unix_assert(how, why)
+ end
+ if not (how == "exit" and why == 0) then
+ io.stderr:write(how .. ":" .. tostring(why).."\n")
+ os.exit(1)
+ end
+end
+
+local function esc_quote_all(t)
+ local tt = {}
+ for i = 1, #t do
+ tt[i] = ("%q"):format(t[i])
+ end
+ return table.concat(tt, " ")
+end
+
+local function load_auth(fname)
+ local fh = io.open(fname, "r")
+ local line = fh:read("*l")
+ local ret = {}
+ while line do
+ line = line:gsub("^ *", "")
+ line = line:gsub(" *$", "")
+ line = line:gsub("^#.*", "")
+ if line ~= "" then
+ local repopath, user, keyset, key =
+ line:match('^[^\\]+\\"([^"]+)\\" \\"([^"]+)\\" \\"([^"]+)\\""[^ ]+ (.+)$')
+ assert(repopath, line)
+ ret[#ret+1] = {
+ repopath = repopath,
+ user = user,
+ keyset = keyset,
+ key = key
+ }
+ ret[key] = ret[#ret]
+ end
+ line = fh:read("*l")
+ end
+ fh:close()
+ return ret
+end
+
+local function generate_exturl(user, key, repo)
+ local authkeys = load_auth(ssh_key_file("testinstance", "authorized_keys"))
+ local pubkey = (sio.open(ssh_key_file(user, key) .. ".pub", "r")):read("*l")
+ local authline = assert(authkeys[pubkey])
+ local extfmt = "ext::env HOME=%s SSH_CLIENT=%s SSH_ORIGINAL_COMMAND=%s %s %s %s %s"
+ local function esc(s)
+ return ((s:gsub("%%", "%%%%")):gsub(" ", "%% "))
+ end
+ return (extfmt):format(esc(user_home("testinstance")),
+ esc("10.0.0.1 1234"),
+ "%S% " .. esc(repo),
+ esc(gitano.config.lib_bin_path() .. "/gitano-auth"),
+ esc(authline.repopath),
+ esc(authline.user), esc(authline.keyset))
+end
+
+function cmd_createunixuser(username)
+ assert(sio.mkdir(user_home(username), "0755"))
+ assert(sio.mkdir(ssh_base(username), "0755"))
+end
+
+function cmd_createsshkey(username, keyname, optionaltype)
+ optionaltype = optionaltype or "rsa"
+ run_program {
+ "ssh-keygen", "-q",
+ "-t", optionaltype,
+ "-C", username .. "-" .. optionaltype .. "@" .. keyname,
+ "-f", ssh_key_file(username, keyname),
+ "-N", "" }
+end
+
+function cmd_setupstandard(owning_user, master_key)
+ local clodname = basedir .. "setup.clod"
+ local fh = io.open(clodname, "w")
+ fh:write('setup.batch "true"\n')
+ fh:write(('paths.pubkey %q\n'):format(ssh_key_file(owning_user, master_key) .. ".pub"))
+ fh:write('site.name "Gitano Test Instance"\n')
+ fh:write('log.prefix "gitano-test"\n')
+ fh:write(('admin.keyname %q\n'):format(master_key))
+ fh:close()
+ run_program {
+ "gitano-setup", clodname,
+ exe = gitano.config.lib_bin_path() .. "/gitano-setup",
+ env = { HOME = user_home(owning_user) }
+ }
+end
+
+function cmd_cloneviassh(user, key, repo, localname)
+ local exturl = generate_exturl(user, key, repo)
+ run_program {
+ "git", "clone", exturl, user_home(user) .. "/" .. localname,
+ }
+end
+
+function cmd_cloneexists(user, localname)
+ run_program {
+ "git", "fsck", user_home(user) .. "/" .. localname
+ }
+end
+
+function cmd_pubkeyfilename(user, key)
+ print(ssh_key_file(user, key) .. ".pub")
+end
+
+function cmd_runcommand(user, key, ...)
+ local authkeys = load_auth(ssh_key_file("testinstance", "authorized_keys"))
+ local pubkey = (sio.open(ssh_key_file(user, key) .. ".pub", "r")):read("*l")
+ local authline = assert(authkeys[pubkey])
+ local cmdline = {
+ gitano.config.lib_bin_path() .. "/gitano-auth",
+ authline.repopath, authline.user, authline.keyset,
+ env = {HOME = user_home("testinstance"), SSH_CLIENT="10.0.0.1 1234"}
+ }
+ cmdline.env.SSH_ORIGINAL_COMMAND = esc_quote_all({...})
+ run_program(cmdline)
+end
+
+function cmd_clonelocation(user, localname)
+ print(user_home(user) .. "/" .. localname)
+end
+
+function cmd_findtoken()
+ local input = sio.stdin:read("*a")
+ local token = input:match("("..("[0-9a-f]"):rep(40)..")")
+ assert(token, "Cannot find a token")
+ print(token)
+end
+
+local cmd = table.remove(argv, 1)
+if _G['cmd_' .. cmd] then
+ _G['cmd_' .. cmd](unpack(argv))
+else
+ error("Unknown command: " .. cmd)
+end
diff --git a/testing/library.yarn b/testing/library.yarn
new file mode 100644
index 0000000..c49021e
--- /dev/null
+++ b/testing/library.yarn
@@ -0,0 +1,104 @@
+<!-- -*- markdown -*- -->
+Test library for Gitano
+=======================
+
+When running tests under yarn, for each scenario, we are provided with a
+temporary working directory called `$DATADIR` which is a fresh directory for
+each scenario being run. Within that base, we can set up any number of fake
+SSH keys, a fake Gitano instance, fake users, and use them to make clones, do
+pushes etc. Nearly all of the implementations rely on a tool in the testing
+directory called `gitano-test-tool` the path to which is available as `$GTT`.
+
+For ease of testing, the fake user who gets to "own" the Gitano instance will
+be called `testinstance` and the keyset which they get to use in order to
+access the repository will be called `adminkey`. This is important when it
+comes to cloning, pushing, etc.
+
+Managing the fake unix users
+----------------------------
+
+ IMPLEMENTS GIVEN a unix user called ([a-z][a-z0-9]*)
+ $GTT createunixuser $MATCH_1
+
+ IMPLEMENTS GIVEN ([a-z][a-z0-9]*) has keys called ([a-z][a-z0-9]*)
+ $GTT createsshkey $MATCH_1 $MATCH_2
+
+General instance management
+---------------------------
+
+ IMPLEMENTS GIVEN a standard instance
+ $GTT createunixuser testinstance
+ $GTT createsshkey testinstance adminkey
+ $GTT setupstandard testinstance adminkey
+
+Repository access
+-----------------
+
+ IMPLEMENTS WHEN ([a-z][a-z0-9]*),? using ([a-z][a-z0-9]*),? clones ([^ ]+) as ([^ ]+)
+ $GTT cloneviassh $MATCH_1 $MATCH_2 "$MATCH_3" "$MATCH_4"
+
+Clone manipulation
+------------------
+
+ IMPLEMENTS THEN ([a-z][a-z0-9]*) has a clone of ([^ ]+)
+ $GTT cloneexists $MATCH_1 "$MATCH_2"
+
+ IMPLEMENTS WHEN git pull happens in ([a-z][a-z0-9]*) ([^ ]+)
+ cd "$($GTT clonelocation $MATCH_1 "$MATCH_2")"
+ git pull
+
+ IMPLEMENTS THEN ([a-z][a-z0-9]*) ([^ ]+) has a file called (.+)
+ cd "$($GTT clonelocation $MATCH_1 "$MATCH_2")"
+ test -r "$MATCH_3"
+
+ IMPLEMENTS THEN ([a-z][a-z0-9]*) ([^ ]+) has no file called (.+)
+ set -x
+ cd "$($GTT clonelocation $MATCH_1 "$MATCH_2")"
+ if test -r "$MATCH_3"; then false; else true; fi
+
+Admin repo manipulation
+-----------------------
+
+ IMPLEMENTS WHEN ([a-z][a-z0-9]*),? using ([a-z][a-z0-9]*),? adds user ([a-z][a-z0-9]*),? using ([a-z][a-z0-9]*) ([a-z][a-z0-9]*)
+ $GTT runcommand $MATCH_1 $MATCH_2 \
+ user add $MATCH_3 $MATCH_3@testinstance "$MATCH_3's real name"
+ $GTT runcommand $MATCH_1 $MATCH_2 \
+ as $MATCH_3 sshkey add default < \
+ $($GTT pubkeyfilename $MATCH_4 $MATCH_5)
+
+ IMPLEMENTS WHEN ([a-z][a-z0-9]*),? using ([a-z][a-z0-9]*),? deletes user ([a-z][a-z0-9]*)
+ TOKEN=$($GTT runcommand $MATCH_1 $MATCH_2 user del $MATCH_3 2>&1 | $GTT findtoken)
+ $GTT runcommand $MATCH_1 $MATCH_2 user del $MATCH_3 $TOKEN
+
+Generic utility methods
+-----------------------
+
+ IMPLEMENTS WHEN ([a-z][a-z0-9]*) ([a-z][a-z0-9]*) runs (.+)
+ $GTT runcommand $MATCH_1 $MATCH_2 $MATCH_3 > $DATADIR/stdout 2> $DATADIR/stderr
+
+ IMPLEMENTS WHEN ([a-z][a-z0-9]*) ([a-z][a-z0-9]*),? expecting failure,? runs (.+)
+ if $GTT runcommand $MATCH_1 $MATCH_2 $MATCH_3 > $DATADIR/stdout 2> $DATADIR/stderr; then
+ false
+ fi
+
+ IMPLEMENTS THEN ([^ ]+) contains (.+)
+ grep -q "$MATCH_2" $DATADIR/"$MATCH_1"
+
+ IMPLEMENTS THEN ([^ ]+) does not contain (.+)
+ if grep -q "$MATCH_2" $DATADIR/"$MATCH_1"; then false; else true; fi
+
+ IMPLEMENTS THEN ([^ ]+) is empty
+ if grep -q . $DATADIR/"$MATCH_1"; then false; fi
+
+ IMPLEMENTS THEN failure ensues
+ cd $DATADIR
+ echo "FIND:"
+ find .
+ echo "KEYS:"
+ cat user-home-testinstance/.ssh/authorized_keys
+ echo "OUT":
+ cat stdout
+ echo "ERR":
+ cat stderr
+ /bin/false
+