% Test suite for ACL on git.liw.fi Introduction ============ This is a test suite for my Gitano ACL setup on git.liw.fi. It is run against either the real or a test instance of the setup. It requires the person running it to have admin access on the Gitano instance, so the tests can create and remove users and repositories. ACL design ========== Requirements ------------ I want to run a git server primarily for my own use. I may later offer hosting of particular repositories for friends, or Soile, but we'll see. I want to have both public and private repositories, and I want to allow others' limited push access to some repos, to make collaboration easier. I don't want others to be able to create repos, I think, but not sure about that yet. I'll want to host all of my free software projects on my server, and also some private repositories, such as my personal journal. Possible design --------------- All access control will be granted via memberships in groups. * `gitano-admin` is the built-in superuser group, whose members can do anything. Most importantly, they can administer users and create top-level repositories. - I am the only admin, at least for now * `trusted` is for people whom I trust to not abuse their priviledges. They can push anything to any public non-personal repository, and can create their own repositories under `personal/${user}/` and `private/${user}/`. * `guest` can push to branches prefixed with `${user}/` in any public repo, and anything to any repo they own. They can't create any repos, but a `gitano-admin` may create one for them. For my free software projects, which are public, anyone can clone them (over the git protocol), and browser their source code (with cgit). If they provide useful patches and want to have an account on my server to make collaboration easier, I can make them a guest account. That allows them to push their changes to a branch, from which I can review and merge them. The trusted group is not meant for making it easier for collaborators to start merging to my free software project master branches. The access control is too coarse for that. It is, instead, meant for allowing friends host their own stuff on my server. Simple design ------------- However, setting up groups and stuff is currently unnecessary. I will instead have a simpler setup: * `gitano-admin` can do anything. * Everyone else can access public repos and push to branches prefixed with their username. * Private repos are under the `private/` prefix, and I'll add my username to allow others to have them in the future. Private repos are not visible via cgit or over the git protocol, and can only be accessed over ssh by a `gitano-admin` (which is only me, for now). This is what the test suite is meant to test. It is a simple design that I can, later, improve upon to add more groups, and give people more detailed control and access. Per-repo groups --------------- In addition to the simple design, I will have a per-repository group concept. For each repository `foo`, if the user is in a group called `foo`, their restriction pushing only to branches prefixed by their username is lifted. This allows me to share a repository with specific people and not treat them as guests, but as first class citizens for that project. Test suite pre-requisites ========================= Yarn must be run with `--env` used to set the environment variables `GITANO` and `GITHOST`. `GITANO` must be the Unix user for the Gitano instance (typically `git`), and `GITHOST` must be the address of the host (IP address or domain name). The `SSH_AUTH_SOCK` environment variable needs to be set so the user running the test suite can access the Gitano instance being tested using their own ssh key. The person running this test suite must be able to log in to the Gitano instance using their normal ssh key. In other words, `ssh "$GITANO@$GITHOST" whoami` must work. See the `check` script for details on how to invoke yarn for this test suite. The test suite will create a user called `tstusr` and `tstusr2`, and remove them after the test suite. The users may get created and removed multiple times. The test suite creates repositories `tstrepo` and `private/tstrepo` and removes them afterwards. Scenarios ========= User creation ------------- The admin must be able to create and remove a user. SCENARIO admin can create and remove a user ASSUMING no tstusr user exists on server GIVEN an ssh key for tstusr WHEN admin creates user tstusr THEN user tstusr exists AND user tstusr can access gitano WHEN admin removes user tstusr THEN user tstusr doesn't exist FINALLY remove user tstusr on server A non-admin mustn't be able to create or remove users. SCENARIO non-admin attempts to create or remove users ASSUMING no tstusr user exists on server AND no tstusr2 user exists on server GIVEN an ssh key for tstusr AND an ssh key for tstusr2 WHEN admin creates user tstusr AND tstusr attempts to create user tstusr2 THEN attempt failed with error matching "You may not perform site administration" WHEN admin creates user tstusr2 AND tstusr attempts to remove user tstusr2 THEN attempt failed with error matching "You may not perform site administration" FINALLY remove user tstusr on server AND remove user tstusr2 on server Public repository creation, access, and removal ----------------------------------------------- The ruleset is meant to make all repositories public. Admin should be able to create a public repository. That repository should then be accessible to both the admin and a non-admin via both git and ssh protocols. Finally, the admin, but not a non-admin, should be able to remove the repository. SCENARIO public repositories ASSUMING no tstusr user exists on server GIVEN an ssh key for tstusr WHEN admin creates user tstusr AND admin creates repository tstrepo THEN admin can see repository tstrepo AND admin can clone tstrepo using git AND admin can clone tstrepo using ssh AND tstusr can see repository tstrepo AND tstusr can clone tstrepo using ssh AND cgit shows repository tstrepo WHEN tstusr attempts to remove repository tstrepo THEN attempt failed with error matching "You may not destroy repositories you do not own" WHEN admin removes repository tstrepo THEN admin can't see repository tstrepo AND admin can't clone tstrepo using git AND admin can't clone tstrepo using ssh AND tstusr can't see repository tstrepo AND tstusr can't clone tstrepo using ssh AND cgit doesn't show repository tstrepo FINALLY remove repository tstrepo on server AND remove user tstusr on server The admin should should be able to push to any branch in a public repo. SCENARIO admin can push to public repository WHEN admin creates repository tstrepo AND admin clones tstrepo over ssh AND admin makes change to master in tstrepo THEN admin can push master in tstrepo WHEN admin makes change to foo in tstrepo THEN admin can push foo in tstrepo WHEN admin tags foo branch in tstrepo THEN admin can push foo with tags in tstrepo FINALLY remove repository tstrepo on server Non-admin, however, shouldn't be able to push to master, but should be able to push to a branch prefixed by their username. SCENARIO non-admin cannot push to master ASSUMING no tstusr user exists on server GIVEN an ssh key for tstusr WHEN admin creates user tstusr AND admin creates repository tstrepo AND tstusr clones tstrepo over ssh AND tstusr makes change to master in tstrepo AND tstusr attempts to push master in tstrepo THEN attempt failed with error matching "Rules refused update" WHEN tstusr makes change to tstusr/foo in tstrepo THEN tstusr can push tstusr/foo in tstrepo FINALLY remove repository tstrepo on server AND remove user tstusr on server However, a non-admin who is in a group with the same name as a repository can push to master. SCENARIO non-admin can push to master if in group for repository ASSUMING no tstusr user exists on server GIVEN an ssh key for tstusr WHEN admin creates user tstusr AND admin creates repository tstrepo AND admin creates group tstrepo AND admin adds user tstusr to group tstrepo AND tstusr clones tstrepo over ssh AND tstusr makes change to master in tstrepo THEN tstusr can push master in tstrepo FINALLY remove repository tstrepo on server AND remove user tstusr on server AND remove group tstrepo on server Private repositories -------------------- A private repository is located under `private/`. It can only be accessed by the admin, and is not served via the git protocol or by cgit. SCENARIO private repositories ASSUMING no tstusr user exists on server GIVEN an ssh key for tstusr WHEN admin creates user tstusr AND admin creates repository private/tstrepo THEN admin can clone private/tstrepo using ssh AND admin can't clone private/tstrepo using git AND admin can see repository private/tstrepo AND tstusr can't clone private/tstrepo using ssh AND tstusr can't see repository private/tstrepo AND cgit doesn't show repository private/tstrepo FINALLY remove repository private/tstrepo on server AND remove user tstusr on server Implementation sections ======================= Check results of attempted operation ------------------------------------ Some scenario steps attempt to do something which may (or should) fail. This step verifies the result of such an attempt. It is intentionally named to be quite generic so we don't need to have multiple "foo failed with error..." steps. IMPLEMENTS THEN attempt failed with error matching "(.*)" echo "stderr of attempted command:" cat "$DATADIR/attempt.stderr" grep "$MATCH_1" "$DATADIR/attempt.stderr" ssh key generation ------------------ Our test users need ssh keys. We generate these on the fly rather than storing them in git, so that if someone gets a copy of this test suite, they don't have keys that can, at least temporarily, access the gitano instance. The key is stored as `$DATADIR/$USERNAME.key` (for the secret key; public key adds `.pub` to the end of the pathname). We run `ssh-keygen` with `-N` to set an empty passphrase. This is OK for test keys that never leave the local system, because our shell library makes sure `$DATADIR` is inaccessible to anyone else. IMPLEMENTS GIVEN an ssh key for (\S+) ssh-keygen -f "$DATADIR/$MATCH_1.key" -N '' Check for users on server ------------------------- We check for users on the server at various stages. Those tests are collected here, since they're all quite similar. Since we do it in several IMPLEMENTS sections, we have a shell function in the shell library to contain the actual code. First of all, we need to verify that there are no test related users on the server. If there is, something's gone wrong in a previous run, and things should be cleaned up manually. Or another run of the test suite is going on, and we shouldn't interfere with that. IMPLEMENTS ASSUMING no (\S+) user exists on server if user_exists "$MATCH_1" then die "User $MATCH_1 exists on server, but shouldn't" fi Verify a user exists on the server. IMPLEMENTS THEN user (\S+) exists user_exists "$MATCH_1" Verify a user doesn't exist on the server. IMPLEMENTS THEN user (\S+) doesn't exist if user_exists "$MATCH_1" then die "User $MATCH_1 exists on server, but shouldn't" fi Verify a user can actually access gitano (by invoking whoami). This is necessary to make sure that user creation added the user's ssh key; otherwise other test steps may fail for unrelated reasons and the test suite may interpret that wrongly. Further, we make sure the user's ssh key can access their account and not some other account. IMPLEMENTS THEN user (\S+) can access gitano run_gitano_as "$MATCH_1" whoami | grep "User name: $MATCH_1\$" User creation ------------- An admin creates a user on the server. Since we need to have a separate step for when a non-admin attempts the same, we have a shell function to do the actual work. The shell function also sets the ssh key for the user. IMPLEMENTS WHEN admin creates user (\S+) user_add admin "$MATCH_1" Attempt to create a user; check later if it worked. IMPLEMENTS WHEN (\S+) attempts to create user (\S+) attempt user_add "$MATCH_1" "$MATCH_2" User removal ------------ Admin removes a user. IMPLEMENTS WHEN admin removes user (\S+) user_del admin "$MATCH_1" Non-admin attempts to remove a user. IMPLEMENTS WHEN (\S+) attempts to remove user (\S+) attempt user_del "$MATCH_1" "$MATCH_2" Admin clean up user at end of scenario. IMPLEMENTS FINALLY remove user (\S+) on server if user_exists "$MATCH_1" then user_del admin "$MATCH_1" fi Group creation and removal -------------------------- An admin creates a group on the server. IMPLEMENTS WHEN admin creates group (\S+) group_add admin "$MATCH_1" An admin adds a user to a group. IMPLEMENTS WHEN admin adds user (\S+) to group (\S+) group_adduser admin "$MATCH_2" "$MATCH_1" An admin removes a group on the server. IMPLEMENTS FINALLY remove group (\S+) on server group_del admin "$MATCH_1" Repository creation ------------------- Repositories can only be created by the admin. IMPLEMENTS WHEN admin creates repository (\S+) run_gitano_as admin create "$MATCH_1" Repository listing ------------------ Can a user see the desired repository in the `gitano ls` output? IMPLEMENTS THEN (\S+) can see repository (\S+) repo_exists "$MATCH_1" "$MATCH_2" And sometimes they shouldn't see it anymore. IMPLEMENTS THEN (\S+) can't see repository (\S+) if repo_exists "$MATCH_1" "$MATCH_2" then die "$MATCH_1 can see repository $MATCH_2 but shouldn't" fi Repository cloning ------------------ Repositories can be cloned using git or ssh protocols, and they may be cloned by various users. We store the clone repositories as `$DATADIR/$USER/$REPO`. If the same user clones the same repository more than once, we only keep the last one. It doesn't matter who clones over git, since git is open to everyone. So we only have a variant for admin, for simplicity. IMPLEMENTS THEN admin can clone (\S+) using git localdir="$DATADIR/admin/$MATCH_1" rm -rf "$localdir" mkdir -p "$localdir" git clone "git://$GITHOST/$MATCH_1" "$localdir" However, cloning over ssh is serious business, for ACL. The tricky bit here is to get git to use the right ssh key. We do this by having a ./ssh script that runs the real ssh, but adds a `-i` option to the desired keyfile. But we only do that for non-admin users. IMPLEMENTS THEN (\S+) can clone (\S+) using ssh clone_with_ssh "$MATCH_1" "$MATCH_2" We also need to be able to check we can't clone. IMPLEMENTS THEN admin can't clone (\S+) using git localdir="$DATADIR/meh" rm -rf "$localdir" if git clone "git://$GITHOST/$MATCH_1" "$localdir" then die "repository $MATCH_1 can be cloned over git, but shouldn't" fi IMPLEMENTS THEN (\S+) can't clone (\S+) using ssh if clone_with_ssh "$MATCH_1" "$MATCH_2" then die "$MATCH_1 can clone $MATCH_2 over ssh, but shouldn't" fi More variants of cloning: this is a WHEN rather than THEN. IMPLEMENTS WHEN (\S+) clones (\S+) over ssh clone_with_ssh "$MATCH_1" "$MATCH_2" Repository removal ------------------ At the end, we need to clean up repositories. IMPLEMENTS FINALLY remove repository (\S+) on server if repo_exists admin "$MATCH_1" then destroy_repo admin "$MATCH_1" fi An admin can remove a repository. IMPLEMENTS WHEN admin removes repository (\S+) destroy_repo admin "$MATCH_1" A non-admin may try to remove a repository. It should fail, but they can try. IMPLEMENTS WHEN (\S+) attempts to remove repository (\S+) attempt destroy_repo "$MATCH_1" "$MATCH_2" Repository changes ------------------ Make a change to a given branch, in a repository cloned by a specific user. We don't really care about what the change is, just that it's there. We also commit the change. If the branch doesn't exist yet, we create it (dealing with empty repos without branches while doing so). IMPLEMENTS WHEN (\S+) makes change to (\S+) in (\S+) cd "$DATADIR/$MATCH_1/$MATCH_3" # Create master if it doesn't exist. if ! git branch | grep . then touch file.txt git add file.txt git commit -m "Initial commit" fi # Create wanted branch, if missing, and change to it. if ! git checkout "$MATCH_2" then git checkout -b "$MATCH_2" fi date >> file.txt git add file.txt git commit -m "A change to file.txt" Make a tag change in a repo. IMPLEMENTS WHEN (\S+) tags (\S+) branch in (\S+) cd "$DATADIR/$MATCH_1/$MATCH_3" git checkout "$MATCH_2" git tag -a -m "a tag" "$MATCH_1.$(date +%s)" Repository pushing ------------------ A named user pushes a branch to a repository. IMPLEMENTS THEN (\S+) can push (\S+) in (\S+) cd "$DATADIR/$MATCH_1/$MATCH_3" git checkout "$MATCH_2" push_with_ssh "$MATCH_1" origin HEAD Push with tags. IMPLEMENTS THEN (\S+) can push (\S+) with tags in (\S+) cd "$DATADIR/$MATCH_1/$MATCH_3" git checkout "$MATCH_2" push_with_ssh "$MATCH_1" --tags origin HEAD Attempt to push, when the outcome may be uncertain. IMPLEMENTS WHEN (\S+) attempts to push (\S+) in (\S+) cd "$DATADIR/$MATCH_1/$MATCH_3" git checkout "$MATCH_2" attempt push_with_ssh "$MATCH_1" origin HEAD Cgit access ----------- Verify that cgit shows a repository. IMPLEMENTS THEN cgit shows repository (\S+) cgit_shows "$MATCH_1" Verify cgit doesn't show a repository. IMPLEMENTS THEN cgit doesn't show repository (\S+) if cgit_shows "$MATCH_1" then die "cgi shows $MATCH_1 but shouldn't" fi