summaryrefslogtreecommitdiff
path: root/000.yarn
blob: 565a43c7bec4ca7753598faf1f649f4d8b0ced35 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
---
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')