summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPedro Alvarez <pedro.alvarez@codethink.co.uk>2014-07-14 15:16:15 +0000
committerPedro Alvarez <pedro.alvarez@codethink.co.uk>2014-07-14 15:16:15 +0000
commit4b8ce6875266fdd6609a217dcf2924d7d4815cc2 (patch)
tree8d7a5edd116bf327df8f507f21eaad428b056998
parentd5a76a5caf51d12c811317ac6e376942b7633770 (diff)
parentef7cb90b7e3df1f953d9190f1193101ac4c2f9db (diff)
downloadgitano-4b8ce6875266fdd6609a217dcf2924d7d4815cc2.tar.gz
Merge branch 'baserock/pedroalvarez/trove-ansible3' into baserock/morphbaserock/morph
Updated to the latest version of gitano, which now allows the user to change the skeleton path.
-rw-r--r--bin/gitano-auth.in142
-rwxr-xr-xbin/gitano-command.cgi.in24
-rw-r--r--bin/gitano-post-receive-hook.in6
-rw-r--r--bin/gitano-pre-receive-hook.in6
-rw-r--r--bin/gitano-setup.in5
-rwxr-xr-xbin/gitano-smart-http.cgi.in3
-rw-r--r--bin/gitano-update-hook.in6
-rw-r--r--lib/gitano/auth.lua4
-rw-r--r--lib/gitano/util.lua24
-rw-r--r--testing/02-commands-config.yarn91
-rw-r--r--testing/gitano-test-tool.in34
-rw-r--r--testing/keys/alice@main_rsa27
-rw-r--r--testing/keys/alice@main_rsa.pub1
-rw-r--r--testing/keys/testinstance@adminkey_rsa27
-rw-r--r--testing/keys/testinstance@adminkey_rsa.pub1
-rw-r--r--testing/keys/testinstance@other_rsa27
-rw-r--r--testing/keys/testinstance@other_rsa.pub1
-rw-r--r--testing/library.yarn8
18 files changed, 272 insertions, 165 deletions
diff --git a/bin/gitano-auth.in b/bin/gitano-auth.in
index 3901166..2190ae0 100644
--- a/bin/gitano-auth.in
+++ b/bin/gitano-auth.in
@@ -20,152 +20,28 @@ local sp = require "luxio.subprocess"
-- @@GITANO_SHARE_PATH
-- @@GITANO_PLUGIN_PATH
-local repo_root, username, keytag = ...
+local repo_root, user, keytag = ...
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
-local parsed_cmdline, warnings = gitano.util.parse_cmdline(cmdline)
-
-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)
-
-if (#warnings > 0) then
- gitano.log.error("Warnings encountered parsing commandline.");
- gitano.log.warn("\t" .. cmdline)
- gitano.log.warn("")
- gitano.log.warn("Parsed as:")
- for i = 1, #parsed_cmdline do
- gitano.log.warn((" =[%2d]> %s"):format(i, parsed_cmdline[i]))
- end
- gitano.log.warn("\nWarnings were:")
- for i = 1, #warnings do
- gitano.log.warn(" * " .. warnings[i])
- end
- gitano.log.warn("")
- gitano.log.fatal("Game over, sorry\n")
-end
-
--- Now load the administration data
-
-local admin_repo = gall.repository.new((repo_root or "") .. "/gitano-admin.git")
-
-if not admin_repo then
- gitano.log.fatal("Unable to locate administration repository. Cannot continue");
-end
-
-local admin_head = admin_repo:get(admin_repo.HEAD)
-
-if not admin_head then
- gitano.log.fatal("Unable to find the HEAD of the administration repository. Cannot continue");
-end
-
-local config, msg = gitano.config.parse(admin_head)
-
-if not config then
- gitano.log.critical("Unable to parse administration repository.")
- gitano.log.critical(" * " .. (msg or "No error?"))
- gitano.log.fatal("Cannot continue")
-end
-
--- Now, are we an admin?
-if config.groups["gitano-admin"].filtered_members[username] then
- -- Yep, so blithely reset logging level
- gitano.log.set_level(start_log_level)
-end
-
-if not config.global.silent then
- -- Not silent, bump to chatty level automatically
- gitano.log.bump_level(gitano.log.level.CHAT)
-end
-
-local repo
-
--- Find the command
+local authorized, cmd, parsed_cmdline, config, env, repo =
+ gitano.auth.is_authorized(user, "ssh", cmdline, repo_root)
+if authorized then
+ local exit = gitano.util.run_command(cmd, cmdline, parsed_cmdline,
+ user, config, env, repo)
-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
- gitano.log.fatal("Unknown command: " .. parsed_cmdline[1])
-end
-
-if cmd.takes_repo then
- repo, parsed_cmdline = cmd.detect_repo(config, parsed_cmdline)
- if not repo and not parsed_cmdline then
- gitano.log.fatal("Failed to acquire repository object")
+ if exit ~= 0 then
+ gitano.log.fatal("Error running command, exiting")
end
-end
-
--- Validate the commandline, massaging it as necessary.
-
-if not cmd.validate(config, repo, parsed_cmdline) then
- gitano.log.fatal("Validation of command line failed")
-end
-
--- Construct our context ready for prep
-local context = {
- source = "ssh",
- user = username,
- keytag = keytag,
-}
-
-local action, reason = cmd.prep(config, repo, parsed_cmdline, context)
-
-if not action then
- gitano.log.crit(reason)
- gitano.log.fatal("Ruleset did not complete cleanly")
-end
-
-if action == "allow" then
- gitano.log.info(reason or "Ruleset permitted action")
-else
- gitano.log.critical(reason)
- gitano.log.fatal("Ruleset denied action. Sorry.")
-end
-
-gitano.log.debug("Welcome to " .. config.global.site_name)
-gitano.log.debug("Running:")
-for i = 1, #parsed_cmdline do
- gitano.log.debug(" => " .. parsed_cmdline[i])
-end
-gitano.log.debug("")
-gitano.log.debug("On behalf of " .. username .. " using key " .. keytag)
-
--- Set up some useful environment variables
-
-local env = {
- ["GITANO_ROOT"] = repo_root,
- ["GITANO_USER"] = username,
- ["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)
-
-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")
+ gitano.log.fatal("Not authorized")
end
gitano.log.syslog.close()
diff --git a/bin/gitano-command.cgi.in b/bin/gitano-command.cgi.in
index 1954635..2fa1db2 100755
--- a/bin/gitano-command.cgi.in
+++ b/bin/gitano-command.cgi.in
@@ -29,26 +29,6 @@ function url_decode(str)
return str
end
-function run_command(cmd, cmdline, parsed_cmdline, user, config, env, repo)
- gitano.log.debug("Welcome to " .. config.global.site_name)
- gitano.log.debug("Running:")
- for i = 1, #parsed_cmdline do
- gitano.log.debug(" => " .. parsed_cmdline[i])
- end
- gitano.log.debug("")
- gitano.log.debug("On behalf of " .. user .. " using key " .. env["GITANO_KEYTAG"])
-
- local how, why = cmd.run(config, repo, parsed_cmdline, env)
-
- if how ~= "exit" or why ~= 0 then
- gitano.log.critical("Error running " .. parsed_cmdline[1] .. ": " .. how)
- return why
- else
- gitano.log.syslog.info(cmdline, "completed successfully")
- return 0
- end
-end
-
if os.getenv("QUERY_STRING") then
local query_string = url_decode(os.getenv("QUERY_STRING"))
local cmdline = query_string
@@ -74,10 +54,10 @@ if os.getenv("QUERY_STRING") then
gitano.log.buffer_output()
local authorized, cmd, parsed_cmdline, config, env, repo =
- gitano.auth.is_authorized(user, "http", cmdline)
+ gitano.auth.is_authorized(user, "http", cmdline, os.getenv("GITANO_ROOT"))
if authorized then
- local exit = run_command(cmd, cmdline, parsed_cmdline,
+ local exit = gitano.util.run_command(cmd, cmdline, parsed_cmdline,
user, config, env, repo)
stdout:write("Status: " .. (exit == 0 and "200 OK" or "400 Bad request")
diff --git a/bin/gitano-post-receive-hook.in b/bin/gitano-post-receive-hook.in
index 3dccfee..df42ffc 100644
--- a/bin/gitano-post-receive-hook.in
+++ b/bin/gitano-post-receive-hook.in
@@ -31,6 +31,12 @@ local username = luxio.getenv("GITANO_USER") or "gitano/anonymous"
local keytag = luxio.getenv("GITANO_KEYTAG") or "unknown"
local project = luxio.getenv("GITANO_PROJECT") or ""
local source = luxio.getenv("GITANO_SOURCE") or "ssh"
+local running = luxio.getenv("GITANO_RUNNING")
+
+-- Check whether we are called through gitano-auth
+if not running then
+ return 0
+end
-- Now load the administration data
gitano.config.repo_path(repo_root)
diff --git a/bin/gitano-pre-receive-hook.in b/bin/gitano-pre-receive-hook.in
index 182554b..2d9ef7e 100644
--- a/bin/gitano-pre-receive-hook.in
+++ b/bin/gitano-pre-receive-hook.in
@@ -31,6 +31,12 @@ local username = luxio.getenv("GITANO_USER") or "gitano/anonymous"
local keytag = luxio.getenv("GITANO_KEYTAG") or "unknown"
local project = luxio.getenv("GITANO_PROJECT") or ""
local source = luxio.getenv("GITANO_SOURCE") or "ssh"
+local running = luxio.getenv("GITANO_RUNNING")
+
+-- Check whether we are called through gitano-auth
+if not running then
+ return 0
+end
-- Now load the administration data
gitano.config.repo_path(repo_root)
diff --git a/bin/gitano-setup.in b/bin/gitano-setup.in
index f31c8f0..038276c 100644
--- a/bin/gitano-setup.in
+++ b/bin/gitano-setup.in
@@ -164,6 +164,9 @@ ask_for("site.name", "Site name", "a random Gitano instance")
ask_for("log.prefix", "Site log prefix", "gitano")
ask_for("use.htpasswd", "Store passwords with htpasswd? (needed for http authentication)", "no")
+ask_for("paths.skel", "Path to skeleton gitano-admin content",
+ gitano.config.share_path() .. "/skel/gitano-admin")
+
gitano.log.chat("Step 2: Gather required content")
gitano.log.info("=> Prepare site config")
@@ -176,7 +179,7 @@ completely_flat["site.conf"] = site_conf:serialise()
-- Acquire the contents of the skeleton gitano-admin repository
gitano.log.info("=> Acquire skeleton gitano-admin")
-local skel_path = gitano.config.share_path() .. "/skel/gitano-admin"
+local skel_path = get "paths.skel"
local skel = assert(sio.opendir(skel_path))
local function acquire(dir, base, path)
gitano.log.ddebug("Acquire skeleton in:", path)
diff --git a/bin/gitano-smart-http.cgi.in b/bin/gitano-smart-http.cgi.in
index 017c4e7..f294b28 100755
--- a/bin/gitano-smart-http.cgi.in
+++ b/bin/gitano-smart-http.cgi.in
@@ -59,7 +59,8 @@ if request_method == "GET" or request_method == "POST" then
local user = os.getenv("REMOTE_USER") or "gitano/anonymous"
local cmdline = parse_request(request_method)
- if cmdline and gitano.auth.is_authorized(user, "http", cmdline) then
+ if cmdline and gitano.auth.is_authorized(user, "http", cmdline,
+ os.getenv("GITANO_ROOT")) then
local proc = subprocess.spawn_simple({"git", "http-backend"})
local exit_code
diff --git a/bin/gitano-update-hook.in b/bin/gitano-update-hook.in
index bb7d8fe..15da2a9 100644
--- a/bin/gitano-update-hook.in
+++ b/bin/gitano-update-hook.in
@@ -35,6 +35,12 @@ local username = luxio.getenv("GITANO_USER") or "gitano/anonymous"
local keytag = luxio.getenv("GITANO_KEYTAG") or "unknown"
local project = luxio.getenv("GITANO_PROJECT") or ""
local source = luxio.getenv("GITANO_SOURCE") or "ssh"
+local running = luxio.getenv("GITANO_RUNNING")
+
+-- Check whether we are called through gitano-auth
+if not running then
+ return 0
+end
-- Now load the administration data
gitano.config.repo_path(repo_root)
diff --git a/lib/gitano/auth.lua b/lib/gitano/auth.lua
index 8c3a4e6..fa30da2 100644
--- a/lib/gitano/auth.lua
+++ b/lib/gitano/auth.lua
@@ -40,6 +40,7 @@ local function set_environment(repo_root, repo, context, transactionid)
["GITANO_PROJECT"] = (repo or {}).name or "",
["GITANO_SOURCE"] = context.source,
["GITANO_TRANSACTION_ID"] = transactionid,
+ ["GITANO_RUNNING"] = "yes",
}
for k, v in pairs(env) do
@@ -49,8 +50,7 @@ local function set_environment(repo_root, repo, context, transactionid)
return env
end
-local function is_authorized(user, source, cmdline)
- local repo_root = os.getenv("GITANO_ROOT")
+local function is_authorized(user, source, cmdline, repo_root)
local keytag = ""
local authorized = false
diff --git a/lib/gitano/util.lua b/lib/gitano/util.lua
index 291c68d..ab8730a 100644
--- a/lib/gitano/util.lua
+++ b/lib/gitano/util.lua
@@ -15,6 +15,28 @@ local tconcat = table.concat
local check_password = scrypt.verify_password
+local function run_command(cmd, cmdline, parsed_cmdline, user,
+ config, env, repo)
+ log.debug("Welcome to " .. config.global.site_name)
+ log.debug("Running:")
+ for i = 1, #parsed_cmdline do
+ log.debug(" => " .. parsed_cmdline[i])
+ end
+ log.debug("")
+ log.debug("On behalf of " .. user .. " using key " .. env["GITANO_KEYTAG"])
+
+ local how, why = cmd.run(config, repo, parsed_cmdline, env)
+
+ if how ~= "exit" or why ~= 0 then
+ log.critical("Error running " .. parsed_cmdline[1] .. ": " .. how)
+ log.critical("Unable to continue")
+ return why
+ else
+ log.syslog.info(cmdline, "completed successfully")
+ return 0
+ end
+end
+
local function hash_password(password)
-- For the moment we are using scrypt,
-- we may decide to use other hash functions in the future
@@ -502,4 +524,6 @@ return {
hash_password = hash_password,
check_password = check_password,
+
+ run_command = run_command,
}
diff --git a/testing/02-commands-config.yarn b/testing/02-commands-config.yarn
index f282cb0..fb8f61f 100644
--- a/testing/02-commands-config.yarn
+++ b/testing/02-commands-config.yarn
@@ -1 +1,92 @@
<!-- -*- markdown -*- -->
+config ---- View and change configuration for a repository (Takes a repo)
+=========================================================================
+
+The `config` command allows the repository's configuration file (clod) to be
+queried and updated from the command line without cloning, editing, and pushing
+the `refs/gitano/admin` branch.
+
+Initially there are three configuration values set for a project, namely that
+of its owner, description and `HEAD` reference. We can check these out
+directly, allowing us to verify the initial configuration. However, the config
+stored is a generic clod configuration so we should also verify everything we
+can about the behaviour from there.
+
+Viewing initial `config` for a repo
+-----------------------------------
+
+Initially configuration for owner, description and `HEAD` are supplied to a
+repository when created. `HEAD` is defaulted to `refs/heads/master` but the
+owner can be set via the person running the create.
+
+ SCENARIO Viewing initial `config` for a repo
+
+ GIVEN a standard instance
+ WHEN testinstance adminkey runs create testrepo
+ AND testinstance adminkey runs config testrepo show
+ THEN stderr is empty
+ AND stdout contains project.owner: admin
+ AND stdout contains project.head: refs/heads/master
+
+Configuration changes stick
+---------------------------
+
+When setting configuration variables we expect those values to stick. As such
+we configure a test repository with a value which is not default and then check
+for it.
+
+ SCENARIO Configuration changes stick
+
+ GIVEN a standard instance
+ WHEN testinstance adminkey runs create testrepo
+ AND testinstance adminkey runs config testrepo set project.head refs/heads/trunk
+ AND testinstance adminkey runs config testrepo show
+ THEN stderr is empty
+ AND stdout contains project.head: refs/heads/trunk
+
+Changes to `HEAD` and description hit the filesystem
+----------------------------------------------------
+
+Since the project's description affects things outside of Gitano, verify that
+when changing configuration, all relevant hook sections are run to update the
+outside world.
+
+ SCENARIO Changes to `HEAD` and description hit the filesystem
+
+ GIVEN a standard instance
+ WHEN testinstance adminkey runs create testrepo
+ AND testinstance adminkey runs config testrepo set project.head refs/heads/trunk
+ AND testinstance adminkey runs config testrepo set project.description foobar
+ THEN server-side testrepo.git file description contains foobar
+ AND server-side testrepo.git file HEAD contains refs/heads/trunk
+
+Manipulating list values is possible
+------------------------------------
+
+Clod can contain lists in values. Lists are always one-level and can have
+new items appended...
+
+ SCENARIO Manipulating list values is possible
+
+ GIVEN a standard instance
+ WHEN testinstance adminkey runs create testrepo
+ AND testinstance adminkey runs config testrepo set foo.* hello
+ AND testinstance adminkey runs config testrepo set foo.* world
+ AND testinstance adminkey runs config testrepo show
+ THEN stderr is empty
+ AND stdout contains foo.i_1: hello
+ AND stdout contains foo.i_2: world
+
+...and removed in any order...
+
+ WHEN testinstance adminkey runs config testrepo rm foo.i_1
+ AND testinstance adminkey runs config testrepo show
+ THEN stderr is empty
+ AND stdout contains foo.i_1: world
+
+ WHEN testinstance adminkey runs config testrepo rm foo.i_1
+ AND testinstance adminkey runs config testrepo show
+ THEN stderr is empty
+ AND stdout does not contain foo.i_
+
+FIXME: Once we have ruleset control, add more here perhaps
diff --git a/testing/gitano-test-tool.in b/testing/gitano-test-tool.in
index 8436dd6..e892474 100644
--- a/testing/gitano-test-tool.in
+++ b/testing/gitano-test-tool.in
@@ -109,12 +109,28 @@ 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", "" }
+ local targetkey = ssh_key_file(username, keyname)
+ local sourcekey = table.concat {
+ "testing/keys/", username, "@", keyname, "_", optionaltype }
+ local fh = io.open(sourcekey, "r")
+ if not fh then
+ run_program {
+ "ssh-keygen", "-q",
+ "-t", optionaltype,
+ "-C", username .. "-" .. optionaltype .. "@" .. keyname,
+ "-f", sourcekey,
+ "-N", "" }
+ fh = assert(io.open(sourcekey, "r"))
+ end
+ local ofh = assert(io.open(targetkey, "w"))
+ ofh:write(fh:read("*a"))
+ fh:close()
+ ofh:close()
+ fh = assert(io.open(sourcekey .. ".pub", "r"))
+ ofh = assert(io.open(targetkey .. ".pub", "w"))
+ ofh:write(fh:read("*a"))
+ fh:close()
+ ofh:close()
end
function cmd_setupstandard(owning_user, master_key)
@@ -122,6 +138,7 @@ function cmd_setupstandard(owning_user, master_key)
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(('paths.repos %q\n'):format(user_home(owning_user) .. "/repos"))
fh:write('site.name "Gitano Test Instance"\n')
fh:write('log.prefix "gitano-test"\n')
fh:write(('admin.keyname %q\n'):format(master_key))
@@ -174,6 +191,11 @@ function cmd_findtoken()
print(token)
end
+function cmd_serverlocation(repo)
+ local h = user_home("testinstance")
+ print(table.concat({h, "repos", repo}, "/"))
+end
+
local cmd = table.remove(argv, 1)
if _G['cmd_' .. cmd] then
_G['cmd_' .. cmd](unpack(argv))
diff --git a/testing/keys/alice@main_rsa b/testing/keys/alice@main_rsa
new file mode 100644
index 0000000..2533124
--- /dev/null
+++ b/testing/keys/alice@main_rsa
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEArgw+4pBPFz7BVbdBtdkKo9TZ3fjnEu8pn+FLp5UpYiQJpIIG
+DG5wDxZAb/ZrP2gL+Ay3vW9UDrRYoC80fCXpxZ5iqz+EEFj1jtrJJEGqR19oByNk
+/CCcb9dmiFwvI9Spd5iC2HXqV8JoCX4cziyo349FB55ljuk+ZOLRTn9dRbU0fJU3
+W4GFpTmNY5jBAb2fPfDFHEuQ2ClzlCDmRq7mhdnUqF7/IFcoo9DonWnl5GOzgGzX
+Kr2fzVV8cYZmAO1fPQ/ODLqny1iEYxccDWMJfJMsUMHePKwS7HMntpgyIBLFuzxy
+wVCr0141Iqn5nn8URxaMTT6631o6xs2PiMWSnwIDAQABAoIBAQCGnEP6uL/i649d
++wkgWwgGo/YI3pvhIgYgeIAp0YybMeIfUMzayoNyt7QIpB5YgOFY7IUjRzpM0SEG
+atv99Ni0FgacCdjbR+JLpV0R5JOM9fYgJzjQY2x6d67+YcW3wZ98NwFj5vbi/yG1
+zcr7jsDhfw5VkSVc/XpbTq2xN4JtCSv+0ulh/NCS0U73dvgvLNHUpP+cJrbCYz/j
+XEoeGr9GxN6jkT8kCD7rcv2+5aGxaYeilhx4uo1l0gtj4CGOcUAvq/y+Ej28XUkO
+HltbLvTHkFLo+UU0VY8nUUbflouxnlqpAdxkJfUOACZdrrh7Qse2b1ztlVIMlbg0
+UAAP5jcZAoGBAOcdRIWpxLs8K24QAms7Zq2Gw4i7xBrQmNPxPCGlybR/204q6TdG
+eGzFtbOHyRBWO+50GSl26D+M+axSX+VVzbCK1wUiZltrkt4x+wqgyIuMAlVLUY+R
+XnWpnHNs7GaC5O+eYQxhsP6GkUhf7GqIB03DIxML/TfDC/4ijNzB+bD1AoGBAMDJ
+7IEVLYW6o1+2d+MBDD7oGQRZloW8hePAL60drmeOpdCBJckeCom6C6ARJDPW+pjM
+mYC6RyfGDlc3NXcik5sTMUrtfwCOcdEvgvYttOwFIuk/U+wIaD+si7EThscvC5oa
+yzNaLrbPfSim0LMsp2M8D3UMS0RIEf1BoOfJx6jDAoGAYHEiKvTRF6DgLqmXmM/M
+5RSbe+9+wgHSBH9iLFhWd2/zQAdAEsThc+J9FFHRYXPaxoLEDT2FZR+bAIHPapAH
+qWgGminktLmLLBWHQMQfa7wdLSKlAlgTJt6EXtZRP+XXSva4YMZTaaMV9TGyIjJp
+edW4STZzkFVgJ8ibJ3P6khECgYB1SmRZJElN0v8SfDD0Ku8IVqzhuJ+bPdc3ePWI
+nUY+Ossmz2vtsBk5Mbdg6wzbfS95RwEdEDe6OwT+itg8YwzqjAKxU0yxSfh1DDLh
+E22/KmDTB3RHZdYG5zMVyIt3I2grmaGG3JcPIa1DzjmqyMAN37yHubMRF8faDNOY
+MWsHgQKBgERVOwB/shfQhMN3peXezA/fcIfKUknDgGkduNAVXgstvc1LgISca7qZ
+WClUv2exSE9+8BVpliLUE2PuV07+o0mH8vlRSAAIanHC/N2uBakTHQL9FiM3h49y
+uNPjUaAOejKbpcRv+4a19nAuVDUOTGMqLvU6gm+ZCAUSwtAqHoAW
+-----END RSA PRIVATE KEY-----
diff --git a/testing/keys/alice@main_rsa.pub b/testing/keys/alice@main_rsa.pub
new file mode 100644
index 0000000..68707f5
--- /dev/null
+++ b/testing/keys/alice@main_rsa.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCuDD7ikE8XPsFVt0G12Qqj1Nnd+OcS7ymf4UunlSliJAmkggYMbnAPFkBv9ms/aAv4DLe9b1QOtFigLzR8JenFnmKrP4QQWPWO2skkQapHX2gHI2T8IJxv12aIXC8j1Kl3mILYdepXwmgJfhzOLKjfj0UHnmWO6T5k4tFOf11FtTR8lTdbgYWlOY1jmMEBvZ898MUcS5DYKXOUIOZGruaF2dSoXv8gVyij0OidaeXkY7OAbNcqvZ/NVXxxhmYA7V89D84MuqfLWIRjFxwNYwl8kyxQwd48rBLscye2mDIgEsW7PHLBUKvTXjUiqfmefxRHFoxNPrrfWjrGzY+IxZKf alice-rsa@main
diff --git a/testing/keys/testinstance@adminkey_rsa b/testing/keys/testinstance@adminkey_rsa
new file mode 100644
index 0000000..4bca3a1
--- /dev/null
+++ b/testing/keys/testinstance@adminkey_rsa
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAuIyfjWjhMBAiIcG4ZvsV86EuWi921321e+F5vQdvOBzyUQr/
+R0PX/598NX4A/x6k7DRLexbA9QWu6QgE+s7LLCQ9YX2fHtenUHPRVW132Y7Nw5/s
+AdxFkIoJko2peqFq+lPQ4fc6US1QL0i5pjj0cVykw27y8KcF3oeb4MVwbPdEhu7+
+uWehUUO0SlM846QCr1J+1PJVtaGXXYCOadwHwJKFy5i+SEHDeiTc6eLiA2PlvTm1
+o6Om45fcIy/xzabsRo0hIFg+4Gt82vl3nFgh6oIvtwvNoYerwvvrf2/2enKmUr0z
+WADzhWiK/UVVkngHSce5P33dn4WkB2ZyXtfGDwIDAQABAoIBAQC1h9W2EpFXZrc8
+P0K1QYxBPq3Kll+u7n+jIJJQJ0z2hDqzDz82CX0he+6A67XtPWZ61aHdrO8W1YVM
+wc+sKdfeTrN1/0yS2QxCbfperrQyc27hW6CZ3+Mpny51UxV/g+In5GRWsYpSqWDz
+cfTzlZiVHc0QVEVyBMkYMIpbGbtR4mgDoUnFhd9LRmcrw9+OgJEkNxetb+jq2g0P
+WaftWTrn9mbANeKHXBulub2dR3xbw5Co1e7yMtkYIcQByTcRDBugkUjQ++I/2oFz
+MIVqFlMUhe8jc3kfq5jnWttkRSsPrq/I/Ang+eUSdcctUhHh+FQsltP9HEcBICHn
+p2piMhkxAoGBAOSmpLYFASBMmmn34S8Kc1smttRp3a4WwsGNl0/445Z1W0dUz39V
+H+1ivD3V3D3gFgXH2RFmD71zofdhhURmbOttvtEtw45sb96HChGRd4OZ3LiEHBvu
+AqreYfigiHdMIuLj0TomNiDhnvQizWHDibNlKGT8zP6ttfUbGcfxEWBJAoGBAM6f
+kz2FzvAcR2ZXgCiIc8Gd8rWurNsDHDq/yhTKuMSmwejfFvlk5SMk7tAkRH0/Wkt+
+3kD4IpkV0ewg9Rz1q8IThg1BTE9qR0N+OVjO5ve1ypnltwUF9i/VBgQ8bTIxX9Qt
+TJygg0IyI0Zs5EfOTXzcF6QEbBkcwjiSYSgd/iOXAoGAbVaOzwenmTVoZaIGSYNa
+1Ey4Au0492Wk7f9ySui+lBU8d+jDbKVdJhwf3gXlUqVUgqElWN+QSU0BN5Wnr6S3
+EwGgzNBwgiuydxvmIa6JEyJBXO63rldraR/8g3Lorvt2dz7vrznUina5lw8JXWWu
+9F08KsaElIimyTWTZ3wMjhkCgYAz42sMhi/jqJZdoxeyFiJLuyiaa5VJIszSDBvp
+gMdJyz7jBjM0yhuo6bt3VcRFV8WLM/8IfcfifdJL5DLp5OAPSuvdJErPnrbqwiYQ
+oVTrXCHW6BNAFbEvbeWm5q3dbvzLwdx9cOnFk+W759ikF7Dp7DObouiqnchAgLIZ
+av7JXQKBgQC/ze3gpuceQqAdCuCuxm1HDRuHLYaUluHPaRTa58R9odwrp25+kQDx
+e7m40FOUtqWqKbc31W9g1wOCL/dL0v/7BBi/I1sejXKAym2ljG4ydLIqwtw7GaLS
+h0927YxoVeamLIFOrEXzGCyFdGPKuJsC494ybsNHLUtggr5hn7biIg==
+-----END RSA PRIVATE KEY-----
diff --git a/testing/keys/testinstance@adminkey_rsa.pub b/testing/keys/testinstance@adminkey_rsa.pub
new file mode 100644
index 0000000..95705b0
--- /dev/null
+++ b/testing/keys/testinstance@adminkey_rsa.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4jJ+NaOEwECIhwbhm+xXzoS5aL3bXfbV74Xm9B284HPJRCv9HQ9f/n3w1fgD/HqTsNEt7FsD1Ba7pCAT6zsssJD1hfZ8e16dQc9FVbXfZjs3Dn+wB3EWQigmSjal6oWr6U9Dh9zpRLVAvSLmmOPRxXKTDbvLwpwXeh5vgxXBs90SG7v65Z6FRQ7RKUzzjpAKvUn7U8lW1oZddgI5p3AfAkoXLmL5IQcN6JNzp4uIDY+W9ObWjo6bjl9wjL/HNpuxGjSEgWD7ga3za+XecWCHqgi+3C82hh6vC++t/b/Z6cqZSvTNYAPOFaIr9RVWSeAdJx7k/fd2fhaQHZnJe18YP testinstance-rsa@adminkey
diff --git a/testing/keys/testinstance@other_rsa b/testing/keys/testinstance@other_rsa
new file mode 100644
index 0000000..32e9cdf
--- /dev/null
+++ b/testing/keys/testinstance@other_rsa
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA18K3qYsgwrEcTJfeY82Ww0jSMl1v1KDhw41CI6/EwPpk160S
+AhPdxpuzTg24Anx+J/b+VdhrQhbXPJFlH3vGSygde7DVBbFHGmGb/h7cFz7Rg4O/
+xkLqUbTDX9tP0V3u2C/sEONXqTaSiiiCb7+E/aS6vf34GsiPMT9UXMWE7ZIiEZIq
+zz2T4b9GM13NR3+jbCOVbQgCiEvylG8GIuska5OTG2e/s1IgMcKvpWXpp9sCk0Oq
+6WAjaI7kgbHE9TqhwA2ADfpPS8xvBKmwGnpcp7qfdLwvPlV6NrFo6o0Atc764khz
+EYOQP1iDXKvurNsGr9YJyc/j6gYIuCBquOjCuwIDAQABAoIBAQCyjw3qPR7eoS6X
+YLQGipUzhmeWkOdE4+QTLytGV2eQgWjFaRDXMVO/0wlgFlBrllXdgzZXGyUg68Ay
++uziUk/30PodbGnPLTh358HuW+GvRyijG3yxep1rAxsRkHGNBpzswzQtgcgBXQ2H
+UyEnlCtesl5tb+pNWB/RFOUfZcOty1ZryV6JJ5JFkiXsujaA69pSGBjpm/cjMP2y
+KSOUxQkS4NZzDAKuQsnehyHKa1+7bp0lXFXU3ZWr/7KkrCjczTdD/F26nZuxbFX6
+8PC+h4lG0fx/nGlqdg/7g75x8htJHKpv4tcwQy7APbRp9HSoRRdcIv9LqzpSs/KW
+71LfubUBAoGBAO00zZDy9YPlsIAr/Wu4je+HNXDmDLltD4Y6UvkUQwbT3WdblG8h
+38jTUcGDVFCuo4ulPuqihOGdK+zj8E9yVQUwVDsz2nKyMaBEIgsACl0yc7glN6Gl
+WDE9H9XycNxub8WEEs0VNmG4PxOhaX36TZSEm1rsQbJkUtgaSjkS/2aBAoGBAOja
+8AYS8sUC1DbuseA+Q1k7deLHWWFmTX3L5pVykTlmERnDCGvHHtzAJ7PFp5NcwDvn
+7W20JsK+KMo3n+4fTwmovKK7wkpdMpx1eiR2JDujAg98Eg37nxqzpcHSVT0fvPNG
+jmOj8Nza6lis/wkOc918q+yCs7GveI05V9IlyKM7AoGAVoAp8pDW/VlWavcfvBea
+Et4wm9IYk8n0nlNIjLJZ2vSJybY4w+oLbHW7W6EjryRwWW1SK0hGwuuI6CMbMC2W
+WYUNQmWfZLIcrMAL1g0WunO6hU11IwpjxdjvchquE4RmWBXYsVbp9Oq2fdcf3CPa
+BK3y5U5AiuhQ2aOEq5mE74ECgYEAhdXwx0z0xE+P8dLX4e9nfk4yv5mcweKu/3LG
+oXcsCTWk9o2mtWvJTVAUgbtFSemxg70WNkupS51IjJHUFmVgZEjbwxzv2xYeFNdg
+0LwmrzBN6uCA8BCDrjE7QF/IJk2rqJgRFywPMKGSuE0WePoZlmAl4NZuud4FCAbB
+d0PIQikCgYAOH6SeG4MB2cvtragsS/p2gw+ItTbMh4hiGZePcS3iAIDSPKHt3ixn
+PwK7pklW8bkkUGrRI7qCHfEC5kAyoV+u1U63CH+/HE3SAtpEevGtFROSrVUNZ1o6
+FcgKw4NB/vy0PVVPBx3onJGEjac0HIwcTG5du1+m3v8DVBhvATssOw==
+-----END RSA PRIVATE KEY-----
diff --git a/testing/keys/testinstance@other_rsa.pub b/testing/keys/testinstance@other_rsa.pub
new file mode 100644
index 0000000..33bb3f8
--- /dev/null
+++ b/testing/keys/testinstance@other_rsa.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDXwrepiyDCsRxMl95jzZbDSNIyXW/UoOHDjUIjr8TA+mTXrRICE93Gm7NODbgCfH4n9v5V2GtCFtc8kWUfe8ZLKB17sNUFsUcaYZv+HtwXPtGDg7/GQupRtMNf20/RXe7YL+wQ41epNpKKKIJvv4T9pLq9/fgayI8xP1RcxYTtkiIRkirPPZPhv0YzXc1Hf6NsI5VtCAKIS/KUbwYi6yRrk5MbZ7+zUiAxwq+lZemn2wKTQ6rpYCNojuSBscT1OqHADYAN+k9LzG8EqbAaelynup90vC8+VXo2sWjqjQC1zvriSHMRg5A/WINcq+6s2wav1gnJz+PqBgi4IGq46MK7 testinstance-rsa@other
diff --git a/testing/library.yarn b/testing/library.yarn
index c49021e..89e8da1 100644
--- a/testing/library.yarn
+++ b/testing/library.yarn
@@ -37,6 +37,14 @@ 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"
+Server-side repository checking for behind-the-scenes work
+----------------------------------------------------------
+
+ IMPLEMENTS THEN server-side ([^ ]+) file ([^ ]+) contains (.+)
+ cd "$($GTT serverlocation $MATCH_1)"
+ grep -q "$MATCH_3" "$MATCH_2"
+
+
Clone manipulation
------------------