gunnar-core 3.0.0

gunnar's repository model, identity table, ACL and path sandbox. gunnar-ssh, gunnar-grpc and gunnar-cli all sit on it.
Documentation
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
//! The ACL, asserted in **both** directions.
//!
//! Measure, never assert, in its most literal form: an `may()` that returns `true` for
//! everything passes every "an admin may push" test ever written, and an
//! `may()` that returns `false` for everything passes every "a reader may not
//! push" test. Every case below therefore has its opposite next to it.

mod common;

use common::Fixture;
use gunnar_core::model::PrincipalKind;
use gunnar_core::{Grant, Level, Principal, RepoRef, Scope, Visibility};

fn principal(fx: &Fixture, id: &str) {
    fx.auth
        .add_principal(Principal::new(id, id, PrincipalKind::User).expect("legal id"))
        .expect("add principal");
}

/// upload-pack needs Read, receive-pack needs Write. This is the check the SSH
/// listener makes, and it must go both ways.
#[test]
fn a_read_principal_is_refused_write_and_an_admin_is_allowed() {
    let fx = Fixture::new();
    let repo = fx.repo("acme", "widget");
    let path = repo.path();

    principal(&fx, "reader");
    principal(&fx, "boss");
    fx.auth
        .grant(Grant::repo("reader", path.clone(), Level::Read))
        .expect("grant");
    fx.auth
        .grant(Grant::repo("boss", path.clone(), Level::Admin))
        .expect("grant");

    // The reader may fetch…
    assert!(fx.auth.may("reader", &repo, Level::Read).expect("acl"));
    // …and may NOT push. This is the assertion the whole crate exists for.
    assert!(!fx.auth.may("reader", &repo, Level::Write).expect("acl"));
    assert!(!fx.auth.may("reader", &repo, Level::Admin).expect("acl"));

    // The admin may do all three, because Level is ordered.
    assert!(fx.auth.may("boss", &repo, Level::Read).expect("acl"));
    assert!(fx.auth.may("boss", &repo, Level::Write).expect("acl"));
    assert!(fx.auth.may("boss", &repo, Level::Admin).expect("acl"));

    // And `require` reports the refusal as an error the SSH layer can return.
    let err = fx
        .auth
        .require("reader", &repo, Level::Write)
        .expect_err("must be denied");
    assert!(err.is_denied(), "{err}");
    assert!(format!("{err}").contains("acme/widget"), "{err}");
    fx.auth
        .require("boss", &repo, Level::Write)
        .expect("the admin is not denied");
}

#[test]
fn a_writer_may_fetch_and_push_but_not_administer() {
    let fx = Fixture::new();
    let repo = fx.repo("acme", "widget");
    principal(&fx, "dev");
    fx.auth
        .grant(Grant::repo("dev", repo.path(), Level::Write))
        .expect("grant");

    assert!(fx.auth.may("dev", &repo, Level::Read).expect("acl"));
    assert!(fx.auth.may("dev", &repo, Level::Write).expect("acl"));
    assert!(!fx.auth.may("dev", &repo, Level::Admin).expect("acl"));
}

/// A principal with no grant at all, and an id nobody registered, are both
/// refused everything, read included. "Unknown" must not mean "read-only".
#[test]
fn an_ungranted_or_unknown_principal_may_do_nothing() {
    let fx = Fixture::new();
    let repo = fx.repo("acme", "widget");
    principal(&fx, "stranger");

    for level in [Level::Read, Level::Write, Level::Admin] {
        assert!(!fx.auth.may("stranger", &repo, level).expect("acl"));
        assert!(!fx.auth.may("does-not-exist", &repo, level).expect("acl"));
        assert!(!fx.auth.may("", &repo, level).expect("acl"));
    }
}

/// A grant on one repository must not leak onto its neighbour. This is the
/// case a map keyed only by principal would get wrong.
#[test]
fn a_repo_grant_does_not_reach_another_repo() {
    let fx = Fixture::new();
    let mine = fx.repo("acme", "mine");
    let yours = fx.repo("acme", "yours");
    principal(&fx, "dev");
    fx.auth
        .grant(Grant::repo("dev", mine.path(), Level::Write))
        .expect("grant");

    assert!(fx.auth.may("dev", &mine, Level::Write).expect("acl"));
    assert!(!fx.auth.may("dev", &yours, Level::Read).expect("acl"));
}

#[test]
fn a_namespace_grant_reaches_every_repo_in_that_namespace_and_no_other() {
    let fx = Fixture::new();
    let one = fx.repo("acme", "one");
    let two = fx.repo("acme", "two");
    let elsewhere = fx.repo("other", "three");
    principal(&fx, "team");
    fx.auth
        .grant(Grant::namespace("team", "acme", Level::Write))
        .expect("grant");

    assert!(fx.auth.may("team", &one, Level::Write).expect("acl"));
    assert!(fx.auth.may("team", &two, Level::Write).expect("acl"));
    assert!(!fx.auth.may("team", &elsewhere, Level::Read).expect("acl"));
    assert!(fx
        .auth
        .may_namespace("team", "acme", Level::Write)
        .expect("acl"));
    assert!(!fx
        .auth
        .may_namespace("team", "other", Level::Read)
        .expect("acl"));
}

/// Nested namespaces are separate tenants: `team` does not inherit into
/// `team/sub`. Pinned deliberately rather than left to be discovered, because
/// the other answer is a privilege escalation nobody wrote down.
#[test]
fn a_namespace_grant_does_not_inherit_into_a_nested_namespace() {
    let fx = Fixture::new();
    let outer = fx.repo("team", "outer");
    let inner = fx.repo("team/sub", "inner");
    principal(&fx, "lead");
    fx.auth
        .grant(Grant::namespace("lead", "team", Level::Admin))
        .expect("grant");

    assert!(fx.auth.may("lead", &outer, Level::Admin).expect("acl"));
    assert!(!fx.auth.may("lead", &inner, Level::Read).expect("acl"));

    // The nested namespace is grantable on its own terms.
    fx.auth
        .grant(Grant::namespace("lead", "team/sub", Level::Read))
        .expect("grant");
    assert!(fx.auth.may("lead", &inner, Level::Read).expect("acl"));
    assert!(!fx.auth.may("lead", &inner, Level::Write).expect("acl"));
}

/// A server-scope admin — what the bootstrap key gets — reaches all of it.
/// Without [`Scope::Server`] the first admin could not create a namespace and
/// the server would boot reachable and useless.
#[test]
fn a_server_admin_reaches_every_namespace_and_repo() {
    let fx = Fixture::new();
    let repo = fx.repo("acme", "widget");
    let elsewhere = fx.repo("other", "thing");
    principal(&fx, "root");
    fx.auth
        .grant(Grant::server("root", Level::Admin))
        .expect("grant");

    assert!(fx.auth.may("root", &repo, Level::Admin).expect("acl"));
    assert!(fx.auth.may("root", &elsewhere, Level::Write).expect("acl"));
    assert!(fx.auth.may_server("root", Level::Admin).expect("acl"));
    assert!(fx
        .auth
        .may_namespace("root", "anything", Level::Admin)
        .expect("acl"));

    // …and a repo-scoped admin is NOT a server admin. The red half.
    principal(&fx, "repo-boss");
    fx.auth
        .grant(Grant::repo("repo-boss", repo.path(), Level::Admin))
        .expect("grant");
    assert!(!fx.auth.may_server("repo-boss", Level::Read).expect("acl"));
    assert!(!fx
        .auth
        .may_namespace("repo-boss", "acme", Level::Read)
        .expect("acl"));
}

/// A public repository is readable by any *registered* principal, and writable
/// by none of them without a grant. The second half is what makes the first
/// safe.
#[test]
fn public_visibility_grants_read_and_never_write() {
    let fx = Fixture::new();
    let repo = fx.repo_with(
        "acme",
        "open",
        gunnar_core::HashKind::Sha256,
        Visibility::Public,
    );
    principal(&fx, "anyone");

    assert!(fx.auth.may("anyone", &repo, Level::Read).expect("acl"));
    assert!(!fx.auth.may("anyone", &repo, Level::Write).expect("acl"));
    assert!(!fx.auth.may("anyone", &repo, Level::Admin).expect("acl"));

    // An unregistered id is still nothing, even on a public repository. Public
    // is not "anyone who types a name": the one identity that reads a public
    // repository without being in the table is the *anonymous* principal, and
    // it is a reserved id nobody else can spell. See
    // `the_anonymous_principal_may_read_public_and_nothing_else` below.
    assert!(!fx.auth.may("ghost", &repo, Level::Read).expect("acl"));

    // Flipping it back to private takes the read away again: the red half.
    fx.catalog
        .set_visibility(&repo.path(), Visibility::Private)
        .expect("set");
    let repo = fx.catalog.open(&repo.path()).expect("reopen");
    assert!(!fx.auth.may("anyone", &repo, Level::Read).expect("acl"));
}

// ───────────────────────────────────────────────────────────────────────────
// The anonymous principal: `Repo.visibility` made to mean something
// ───────────────────────────────────────────────────────────────────────────

/// **Rule 0 of `may`, in every direction it has.**
///
/// One `true` and eleven `false`s, and the `true` is what makes the `false`s
/// worth anything: an anonymous rule that answered `false` to everything would
/// satisfy every refusal below and serve no stranger at all, and one that
/// answered `true` to everything would satisfy nothing but would also be
/// invisible in a suite that only ever asserted the public read.
#[test]
fn the_anonymous_principal_may_read_public_and_nothing_else() {
    let fx = Fixture::new();
    let open = fx.repo_with(
        "acme",
        "open",
        gunnar_core::HashKind::Sha256,
        Visibility::Public,
    );
    let shut = fx.repo_with(
        "acme",
        "shut",
        gunnar_core::HashKind::Sha256,
        Visibility::Private,
    );
    let anon = gunnar_core::ANONYMOUS_PRINCIPAL_ID;

    // The one thing it may do.
    assert!(
        fx.auth.may(anon, &open, Level::Read).expect("acl"),
        "an anonymous caller must be able to READ a public repository. That is the whole of \
         what `Repo.visibility` is for"
    );

    // Everything else on the same, public repository.
    assert!(!fx.auth.may(anon, &open, Level::Write).expect("acl"));
    assert!(!fx.auth.may(anon, &open, Level::Admin).expect("acl"));

    // Every level on a private one, including read.
    for level in [Level::Read, Level::Write, Level::Admin] {
        assert!(
            !fx.auth.may(anon, &shut, level).expect("acl"),
            "anonymous reached a PRIVATE repository at {level:?}"
        );
    }

    // The control-plane questions, which are never anonymous at any level.
    for level in [Level::Read, Level::Write, Level::Admin] {
        assert!(!fx.auth.may_namespace(anon, "acme", level).expect("acl"));
        assert!(!fx.auth.may_server(anon, level).expect("acl"));
    }

    // And the flip, on the live catalog: making the open one private takes the
    // anonymous read away, and making the shut one public grants it. Without
    // this pair, a `may` that ignored `visibility` entirely and hard-coded the
    // anonymous answer would pass everything above.
    fx.catalog
        .set_visibility(&open.path(), Visibility::Private)
        .expect("set");
    fx.catalog
        .set_visibility(&shut.path(), Visibility::Public)
        .expect("set");
    let open = fx.catalog.open(&open.path()).expect("reopen");
    let shut = fx.catalog.open(&shut.path()).expect("reopen");
    assert!(!fx.auth.may(anon, &open, Level::Read).expect("acl"));
    assert!(fx.auth.may(anon, &shut, Level::Read).expect("acl"));
}

/// **The reserved id, from all four doors it could be walked through.**
///
/// Rule 0 is a statement about a *name*. It is only worth anything if the name
/// cannot be claimed. A registered principal called `anonymous` would be handed
/// the public-read rule and could then be given keys and grants on top of it,
/// which is an authorisation bypass with a green suite.
#[test]
fn the_anonymous_id_is_reserved_and_can_hold_neither_key_nor_grant() {
    let fx = Fixture::new();
    let repo = fx.repo("acme", "widget");
    let anon = gunnar_core::ANONYMOUS_PRINCIPAL_ID;

    // 1. The constructor.
    let err = Principal::new(anon, "me", PrincipalKind::User)
        .expect_err("`anonymous` must not be constructible as an ordinary principal");
    assert!(format!("{err}").contains("reserved"), "{err}");

    // 2. The table, handed the one value that *can* be built.
    let err = fx
        .auth
        .add_principal(Principal::anonymous())
        .expect_err("`anonymous` must not be registrable");
    assert!(format!("{err}").contains("reserved"), "{err}");

    // 3. A key. There is no principal to hang it on, and that is the refusal.
    const KEY: &str =
        "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMW4aqVr75w+3DWorOCbfwl5XCDCq1yCQ6puzU4qzol2 anon";
    let err = fx
        .auth
        .register_key(anon, KEY, None)
        .expect_err("no key may ever authenticate AS anonymous");
    assert!(format!("{err}").contains("no such principal"), "{err}");

    // 4. A grant.
    let err = fx
        .auth
        .grant(Grant::repo(anon, repo.path(), Level::Admin))
        .expect_err("anonymous must not be grantable");
    assert!(format!("{err}").contains("no such principal"), "{err}");

    // The red half of all four: the identical operations succeed for an
    // ordinary id, so none of the refusals above is a table that simply
    // refuses everything.
    principal(&fx, "ordinary");
    fx.auth
        .register_key("ordinary", KEY, None)
        .expect("an ordinary principal takes a key");
    fx.auth
        .grant(Grant::repo("ordinary", repo.path(), Level::Admin))
        .expect("an ordinary principal takes a grant");
    assert!(fx.auth.may("ordinary", &repo, Level::Admin).expect("acl"));

    // And nothing landed in the table under the reserved name.
    assert!(fx.auth.principal(anon).expect("read").is_none());
    assert!(fx.auth.grants(Some(anon)).expect("read").is_empty());
    assert!(
        fx.auth
            .keys()
            .expect("read")
            .iter()
            .all(|key| key.principal_id != anon),
        "a key is registered to the anonymous principal"
    );
}

/// The anonymous principal is a *value*, and its shape is pinned: id, kind and
/// the predicate agree. A `is_anonymous` keyed on something the ACL does not
/// look at would be a guard watching the wrong field.
#[test]
fn the_anonymous_principal_value_is_what_the_acl_is_keyed_on() {
    let anon = Principal::anonymous();
    assert_eq!(anon.id, gunnar_core::ANONYMOUS_PRINCIPAL_ID);
    assert_eq!(anon.kind, PrincipalKind::Anonymous);
    assert!(anon.is_anonymous());

    // The red half: an ordinary principal is not anonymous, and no other kind
    // claims to be.
    let ordinary = Principal::new("dev", "dev", PrincipalKind::User).expect("legal");
    assert!(!ordinary.is_anonymous());
    assert_ne!(ordinary.kind, PrincipalKind::Anonymous);
}

/// Granting again on the same scope replaces rather than accumulates, so a
/// downgrade is really a downgrade.
#[test]
fn a_downgrade_is_a_downgrade() {
    let fx = Fixture::new();
    let repo = fx.repo("acme", "widget");
    let path = repo.path();
    principal(&fx, "dev");

    fx.auth
        .grant(Grant::repo("dev", path.clone(), Level::Admin))
        .expect("grant");
    assert!(fx.auth.may("dev", &repo, Level::Admin).expect("acl"));

    fx.auth
        .grant(Grant::repo("dev", path.clone(), Level::Read))
        .expect("grant");
    assert_eq!(fx.auth.grants(Some("dev")).expect("grants").len(), 1);
    assert!(fx.auth.may("dev", &repo, Level::Read).expect("acl"));
    assert!(!fx.auth.may("dev", &repo, Level::Write).expect("acl"));

    // And revoking removes it entirely.
    assert!(fx
        .auth
        .revoke_grant("dev", &Scope::Repo(path.clone()))
        .expect("revoke"));
    assert!(!fx.auth.may("dev", &repo, Level::Read).expect("acl"));
    assert!(!fx
        .auth
        .revoke_grant("dev", &Scope::Repo(path))
        .expect("revoke again"));
}

#[test]
fn removing_a_principal_removes_its_grants() {
    let fx = Fixture::new();
    let repo = fx.repo("acme", "widget");
    principal(&fx, "leaver");
    fx.auth
        .grant(Grant::repo("leaver", repo.path(), Level::Admin))
        .expect("grant");
    assert!(fx.auth.may("leaver", &repo, Level::Admin).expect("acl"));

    fx.auth.remove_principal("leaver").expect("remove");
    assert!(fx.auth.grants(None).expect("grants").is_empty());
    assert!(!fx.auth.may("leaver", &repo, Level::Read).expect("acl"));
}

#[test]
fn a_grant_to_an_unknown_principal_is_refused() {
    let fx = Fixture::new();
    let repo = fx.repo("acme", "widget");
    let err = fx
        .auth
        .grant(Grant::repo("ghost", repo.path(), Level::Admin))
        .expect_err("must refuse");
    assert!(format!("{err}").contains("no such principal"), "{err}");
    assert!(fx.auth.grants(None).expect("grants").is_empty());
}

/// The level ordering the whole ACL rests on.
#[test]
fn levels_are_ordered() {
    assert!(Level::Read < Level::Write);
    assert!(Level::Write < Level::Admin);
    assert_eq!(
        [Level::Admin, Level::Read, Level::Write].iter().max(),
        Some(&Level::Admin)
    );
}

#[test]
fn scopes_cover_what_they_say() {
    let fx = Fixture::new();
    let repo = fx.repo("acme", "widget");
    assert!(Scope::Server.covers(&repo));
    assert!(Scope::Namespace("acme".into()).covers(&repo));
    assert!(!Scope::Namespace("acm".into()).covers(&repo));
    assert!(Scope::Repo(RepoRef::new("acme", "widget").unwrap()).covers(&repo));
    assert!(!Scope::Repo(RepoRef::new("acme", "widgets").unwrap()).covers(&repo));
    assert!(!Scope::Repo(RepoRef::new("acme", "widget").unwrap()).covers_namespace("acme"));
}