diff options
author | Richard Ipsum <richard.ipsum@codethink.co.uk> | 2013-10-04 11:41:36 +0100 |
---|---|---|
committer | Richard Ipsum <richard.ipsum@codethink.co.uk> | 2013-10-04 11:41:36 +0100 |
commit | 3791ac2aab5cc13fba01c18857e7cb0d7636b2e1 (patch) | |
tree | 77d24ddbe809c887036ea19c197a2fac9baac8ca | |
parent | 8397ee2115198b022991e2841ec702040a4617ff (diff) | |
parent | 4cb991a303e8826da72244e0ccfb40fea376260d (diff) | |
download | gitano-3791ac2aab5cc13fba01c18857e7cb0d7636b2e1.tar.gz |
Merge branch 'master' into baserock/morph
31 files changed, 811 insertions, 11 deletions
@@ -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 + |