--- title: "A Gitano ruleset: tests" author: Lars Wirzenius (liw@liw.fi) date: 2017-02-05 ... Introduction ============================================================================= [Gitano][gitano] is the server software I use for my [personal git server][gitliwfi], and also at [work][ql]. One of Gitano's primary features is a strong language for defining access control, called [Lace][lace]. However, with great power comes long shoelaces that get easily loosened. This document explains what my ruleset does, and why. It is also a test suite for the ruleset, to make sure it does what it's supposed to, but avoids some of the more obvious pitfalls. [Gitano]: https://www.gitano.org.uk/ [Lace]: https://www.gitano.org.uk/lace/ [gitliwfi]: http://git.liw.fi/ [ql]: http://qvarnlabs.com/ Discussion ----------------------------------------------------------------------------- * There's going to be repositories that are public to the world (in a read-only manner) and those that are meant to be private within a group of people (e.g., all of company staff, or just operations staff). * Some people will have full write access to some repositories, whereas everyone else (even those with Gitano accounts) have only read access to those respositories. * When outsiders have write access, it may need to be restricted, such that they can do things, but can't change master or other branches themselves, or create release tags. We call these guests. * There's automated system, such as CI, which only ever need read-only access. (If we find a need for a bot with write access later, we'll re-think then. YAGNI.) Rough outline for ruleset ----------------------------------------------------------------------------- * Only a Gitano admin can create users, groups, and repositories, or add users to groups. * All repository access is controlled via group membership. Each repository is assumed to have three group associated with it: `writers`, `readers`, and `guests`. These are per-repository configuration variables, whose value should be the name of the Gitano group that defines who are writers, readers, or guests, correspondingly. For example, if `qvarn.git` has `writers` defined as `qvarnlabs-staff`, then only those in the `qvarnlabs-staff` group have full write access. * All groups have full read access. (Git doesn't really allow limiting read at a more granular scale than a repository.) * Writers have full write access. They can push changes, and update any branches and tags. * Guests are like writers, but can only update branches and tags prefixed by their Gitano username. * Public repositories are marked by setting the configuration variable `public` to `yes`. Anyone can clone public repositories, pull from them, and cgit shows them to anyone. Public repositories are published via the anonymous git prototocol. The ruleset change to standard Gitano ruleset ============================================================================= The following lines added to `rules/project.lace` in the `gitano-admin.git` repository implement the ruleset change, on top of the standard Gitano ruleset. EXAMPLE define repo_is_public config/public exact yes allow "Everyone can read a public repo" op_read repo_is_public define user_is_repo_reader group exact ${config/readers} allow "Readers may read" op_read user_is_repo_reader define user_is_repo_writer group exact ${config/writers} allow "Writers may read and write" op_is_basic user_is_repo_writer allow "Writers may update any branch" op_is_normal user_is_repo_writer define user_is_repo_guest group exact ${config/guests} define branch_is_for_user ref prefix refs/heads/${user}/ define tag_is_for_user ref prefix refs/tags/${user}/ allow "Guests may read and write" op_is_basic user_is_repo_guest allow "Guests may update their own branches" op_is_normal user_is_repo_guest branch_is_for_user allow "Guests may update their own tags" op_is_normal user_is_repo_guest tag_is_for_user Use cases as automated test scenarios ============================================================================= These [yarn][yarn] scenarios need to be run as a Gitano user who is in the `gitano-admins` group so that they can create users, and do other priviledged operations. Further, this should be done against a fresh Gitano, without the test users etc. [yarn]: http://liw.fi/cmdtest/ Personas for use cases ----------------------------------------------------------------------------- * **Ian Inhouse Developer** is a staff member and works on a free software project with the random silly name Qvarn. * **Olive Opshead** is a sysadmin and needs to maintain ops related repositories, some of which are sensitive. * **Steven Sect** is the company secretary and needs to access some private respositories with internal company data. * **Gabriella Guest** is an outside developer, collaborating on Qvarn with Ian. Gabriella has been granted restricted commit access to the Qvarn repository. * **CI** is an automated system that monitors some repositories to build, test, and deliver software. Ian makes a bug fix in Qvarn ----------------------------------------------------------------------------- SCENARIO Ian can make a bug fix in Qvarn WHEN admin creates user ian AND admin creates group qvarndevs AND admin adds ian to qvarndevs AND admin creates repository qvarn AND admin sets qvarn config writers to qvarndevs THEN ian can clone qvarn WHEN ian creates qvarn branch bugfix AND ian changes qvarn branch bugfix THEN ian can push qvarn WHEN ian merges qvarn branch bugfix to master THEN ian can push qvarn FINALLY admin removes things that were created Steven can see Qvarn, but not push changes ----------------------------------------------------------------------------- SCENARIO Steven can clone Qvarn but not push changes WHEN admin creates user steven AND admin creates group qvarn-readers AND admin adds steven to qvarn-readers AND admin creates repository qvarn AND admin sets qvarn config readers to qvarn-readers AND admin sets qvarn config writers to qvarn-writers THEN steven can clone qvarn WHEN steven creates qvarn branch bugfix AND steven changes qvarn branch bugfix AND steven merges qvarn branch bugfix to master THEN steven cannot push qvarn FINALLY admin removes things that were created Ian can make a Qvarn release ----------------------------------------------------------------------------- SCENARIO Ian can make a Qvarn release WHEN admin creates user ian AND admin creates group qvarndevs AND admin adds ian to qvarndevs AND admin creates repository qvarn AND admin sets qvarn config writers to qvarndevs THEN ian can clone qvarn WHEN ian creates qvarn branch bugfix AND ian changes qvarn branch bugfix THEN ian can push qvarn WHEN ian merges qvarn branch bugfix to master AND ian tags qvarn master branch with qvarn-1.0 THEN ian can push qvarn with tags FINALLY admin removes things that were created Steven can't push a release tag ----------------------------------------------------------------------------- SCENARIO Steven can't tag a Qvarn release WHEN admin creates user steven AND admin creates group qvarn-readers AND admin creates group qvarn-writers AND admin adds steven to qvarn-readers AND admin creates repository qvarn AND admin sets qvarn config readers to qvarn-readers AND admin sets qvarn config writers to qvarn-writers THEN steven can clone qvarn WHEN steven tags qvarn master branch with qvarn-2.0 THEN steven cannot push qvarn with tags FINALLY admin removes things that were created Gabriella can push changes and tag with her own prefix ----------------------------------------------------------------------------- SCENARIO Gabriella can change and tag her own branch WHEN admin creates user gabriella AND admin creates group qvarn-guests AND admin adds gabriella to qvarn-guests AND admin creates repository qvarn AND admin sets qvarn config guests to qvarn-guests THEN gabriella can clone qvarn WHEN gabriella creates qvarn branch gabriella/bugfix AND gabriella changes qvarn branch gabriella/bugfix THEN gabriella can push qvarn WHEN gabriella tags qvarn gabriella/bugfix branch with gabriella/works THEN gabriella can push qvarn with tags FINALLY admin removes things that were created Gabriella can't push changes to master ----------------------------------------------------------------------------- SCENARIO Gabriella can't push changes to master WHEN admin creates user gabriella AND admin creates group qvarn-guests AND admin adds gabriella to qvarn-guests AND admin creates repository qvarn AND admin sets qvarn config guests to qvarn-guests THEN gabriella can clone qvarn WHEN gabriella creates qvarn branch gabriella/bugfix AND gabriella changes qvarn branch gabriella/bugfix AND gabriella merges qvarn branch gabriella/bugfix to master THEN gabriella cannot push qvarn FINALLY admin removes things that were created Gabriella can't push release tag ----------------------------------------------------------------------------- SCENARIO Gabriella can't push release tag WHEN admin creates user gabriella AND admin creates group qvarn-guests AND admin adds gabriella to qvarn-guests AND admin creates repository qvarn AND admin sets qvarn config guests to qvarn-guests THEN gabriella can clone qvarn WHEN gabriella tags qvarn master branch with qvarn-42.0 THEN gabriella cannot push qvarn with tags FINALLY admin removes things that were created Steven can't read the ops/secrets repo ----------------------------------------------------------------------------- SCENARIO Steven can't clone ops/secrets WHEN admin creates user steven AND admin creates repository ops/secrets THEN steven cannot clone ops/secrets FINALLY admin removes things that were created A public repository can be clone with the anonymous git protocol ----------------------------------------------------------------------------- SCENARIO everyone can clone a public repository WHEN admin creates repository qvarn AND admin sets qvarn config public to yes THEN we can clone qvarn via the git protocol FINALLY admin removes things that were created Scenario step implementations ============================================================================= WHEN admin creates user ----------------------------------------------------------------------------- IMPLEMENTS WHEN admin creates user (\S+) username = get_next_match() append('users', username) gitano( None, 'user add {} {}@example.com Test {}'.format( username, username, username)) pubkey = ssh_keygen(username) gitano(None, 'as {} sshkey add default'.format(username), stdin=pubkey) WHEN admin creates group ----------------------------------------------------------------------------- IMPLEMENTS WHEN admin creates group (\S+) group = get_next_match() append('groups', group) gitano(None, 'group add {} Test group'.format(group)) WHEN admin adds user to group ----------------------------------------------------------------------------- IMPLEMENTS WHEN admin adds (\S+) to (\S+) user = get_next_match() group = get_next_match() gitano( None, 'group adduser {} {}'.format(group, user)) WHEN admin created repository ----------------------------------------------------------------------------- IMPLEMENTS WHEN admin creates repository (\S+) repo = get_next_match() append('repositories', repo) gitano(None, 'create {}'.format(repo)) # Make a commit in master in the repo, push that. user = 'admin' url = repo_ssh_url(repo) dirname = local_checkout_dirname(user, repo) git_as_checked(None, ['clone', url, dirname]) cliapp.runcmd(['git', 'config', 'user.email', user], cwd=dirname) cliapp.runcmd(['git', 'config', 'user.name', user], cwd=dirname) with open(os.path.join(dirname, 'foo.txt'), 'a') as f: f.write('') cliapp.runcmd(['git', 'add', 'foo.txt'], cwd=dirname) cliapp.runcmd(['git', 'commit', '-mfoo'], cwd=dirname) git_as_checked(None, ['push', '--all'], cwd=dirname) WHEN admin sets repository config ----------------------------------------------------------------------------- IMPLEMENTS WHEN admin sets (\S+) config (\S+) to (\S+) repo = get_next_match() key = get_next_match() value = get_next_match() gitano(None, 'config {} set {} {}'.format(repo, key, value)) THEN a user can clone a repository ----------------------------------------------------------------------------- IMPLEMENTS THEN (\S+) can clone (\S+) user = get_next_match() repo = get_next_match() url = repo_ssh_url(repo) dirname = local_checkout_dirname(user, repo) git_as_checked(user, ['clone', url, dirname]) cliapp.runcmd(['git', 'config', 'user.email', user], cwd=dirname) cliapp.runcmd(['git', 'config', 'user.name', user], cwd=dirname) THEN a user can't clone a repository ----------------------------------------------------------------------------- IMPLEMENTS THEN (\S+) cannot clone (\S+) user = get_next_match() repo = get_next_match() url = repo_ssh_url(repo) dirname = local_checkout_dirname(user, repo) exit, out, err = git_as(user, ['clone', url, dirname]) assertNotEqual(exit, 0) THEN a repository can be cloned over the git protocol ----------------------------------------------------------------------------- IMPLEMENTS THEN we can clone (\S+) via the git protocol repo = get_next_match() server = os.environ['GITANO_SERVER'] url = 'git://{}/{}'.format(server, repo) dirname = local_checkout_dirname('anonymous', repo) cliapp.runcmd(['git', 'clone', url, dirname]) WHEN a user creates a local branch ----------------------------------------------------------------------------- IMPLEMENTS WHEN (\S+) creates (\S+) branch (\S+) user = get_next_match() repo = get_next_match() branch = get_next_match() dirname = local_checkout_dirname(user, repo) cliapp.runcmd(['git', 'checkout', '-b', branch], cwd=dirname) WHEN a user changes a branch locally ----------------------------------------------------------------------------- IMPLEMENTS WHEN (\S+) changes (\S+) branch (\S+) user = get_next_match() repo = get_next_match() branch = get_next_match() dirname = local_checkout_dirname(user, repo) with open(os.path.join(dirname, 'foo.txt'), 'a') as f: f.write('foo\n') cliapp.runcmd(['git', 'add', 'foo.txt'], cwd=dirname) cliapp.runcmd(['git', 'commit', '-mfoo'], cwd=dirname) THEN a user can push all local branches ----------------------------------------------------------------------------- IMPLEMENTS THEN (\S+) can push (\S+) user = get_next_match() repo = get_next_match() url = repo_ssh_url(repo) dirname = local_checkout_dirname(user, repo) git_as_checked(user, ['push', '--all', 'origin'], cwd=dirname) THEN a user can push all local tags ----------------------------------------------------------------------------- IMPLEMENTS THEN (\S+) can push (\S+) with tags user = get_next_match() repo = get_next_match() url = repo_ssh_url(repo) dirname = local_checkout_dirname(user, repo) git_as_checked(user, ['push', '--tags', 'origin'], cwd=dirname) THEN a user can't push local branches ----------------------------------------------------------------------------- IMPLEMENTS THEN (\S+) cannot push (\S+) user = get_next_match() repo = get_next_match() dirname = local_checkout_dirname(user, repo) exit, out, err = git_as( user, ['push', '--all', 'origin'], cwd=dirname) sys.stdout.write(out) sys.stderr.write(err) assertNotEqual(exit, 0) THEN a user can't push local tags ----------------------------------------------------------------------------- IMPLEMENTS THEN (\S+) cannot push (\S+) with tags user = get_next_match() repo = get_next_match() dirname = local_checkout_dirname(user, repo) exit, out, err = git_as( user, ['push', '--tags', 'origin'], cwd=dirname) assertNotEqual(exit, 0) WHEN a user merges a branch locally ----------------------------------------------------------------------------- IMPLEMENTS WHEN (\S+) merges (\S+) branch (\S+) to (\S+) user = get_next_match() repo = get_next_match() branch_from = get_next_match() branch_to = get_next_match() dirname = local_checkout_dirname(user, repo) cliapp.runcmd(['git', 'checkout', branch_to], cwd=dirname) cliapp.runcmd(['git', 'merge', branch_from], cwd=dirname) WHEN a user creates a local tag ----------------------------------------------------------------------------- IMPLEMENTS WHEN (\S+) tags (\S+) (\S+) branch with (\S+) user = get_next_match() repo = get_next_match() branch = get_next_match() tag = get_next_match() dirname = local_checkout_dirname(user, repo) cliapp.runcmd(['git', 'tag', '-mrelease!', tag], cwd=dirname) FINALLY clean up anything created during tests ----------------------------------------------------------------------------- IMPLEMENTS FINALLY admin removes things that were created def iter(var, prefix): items = vars[var] or [] for item in items: gitano_confirm_with_token(prefix, item) iter('users', 'user del') iter('groups', 'group del') iter('repositories', 'destroy')