vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
|
ec5c519c
Keenan Nemetz
17d ago
|
1 | package repo |
| 2 | ||
| 3 | // The upgrade path: an instance that stored access rules in refs/vole/acl keeps them | |
| 4 | // when the rules move into the database. That has to be measured, because the failure | |
| 5 | // mode is silent — a repository with no rules denies everybody, admins included. | |
| 6 | ||
| 7 | import ( | |
|
1948358f
Keenan Nemetz
15d ago
|
8 | "os" |
|
ec5c519c
Keenan Nemetz
17d ago
|
9 | "path/filepath" |
| 10 | "testing" | |
|
642e5994
Keenan Nemetz
15d ago
|
11 | "time" |
|
ec5c519c
Keenan Nemetz
17d ago
|
12 | |
|
d37d301f
Keenan Nemetz
23h ago
|
13 | "rodent.software/vole/core/git" |
| 14 | "rodent.software/vole/forge/access" | |
|
ec5c519c
Keenan Nemetz
17d ago
|
15 | ) |
| 16 | ||
| 17 | // withACLs returns a store whose rules live in a database beside the repositories, the | |
| 18 | // way the instance has them. | |
| 19 | func withACLs(t *testing.T) (*Store, *access.Store) { | |
| 20 | t.Helper() | |
| 21 | root := t.TempDir() | |
| 22 | acls, err := access.Open(filepath.Join(root, "users.db")) | |
| 23 | if err != nil { | |
| 24 | t.Fatal(err) | |
| 25 | } | |
|
b1499c72
Keenan Nemetz
13d ago
|
26 | acls.AuditPath = filepath.Join(root, "audit.log") |
|
ec5c519c
Keenan Nemetz
17d ago
|
27 | t.Cleanup(func() { acls.Close() }) |
| 28 | return New(filepath.Join(root, "repos"), "", acls), acls | |
| 29 | } | |
| 30 | ||
| 31 | // legacyWriteACL does what the old code did: a commit on refs/vole/acl holding | |
|
c5902451
Keenan Nemetz
17d ago
|
32 | // access.json. It is how a pre-upgrade repository looks. |
| 33 | func legacyWriteACL(t *testing.T, m *Meta, a *access.ACL) { | |
|
ec5c519c
Keenan Nemetz
17d ago
|
34 | t.Helper() |
| 35 | g := m.Git() | |
| 36 | raw, err := a.Bytes() | |
| 37 | if err != nil { | |
| 38 | t.Fatal(err) | |
| 39 | } | |
| 40 | blob, err := g.HashObject(raw) | |
| 41 | if err != nil { | |
| 42 | t.Fatal(err) | |
| 43 | } | |
|
c5902451
Keenan Nemetz
17d ago
|
44 | tree, err := g.MkTree([]git.Entry{{Mode: "100644", Type: "blob", SHA: blob, Name: ACLPath}}) |
|
ec5c519c
Keenan Nemetz
17d ago
|
45 | if err != nil { |
| 46 | t.Fatal(err) | |
| 47 | } | |
| 48 | commit, err := g.CommitTree(tree, "", "acl: update by keenan") | |
| 49 | if err != nil { | |
| 50 | t.Fatal(err) | |
| 51 | } | |
| 52 | if err := g.UpdateRef(ACLRef, commit); err != nil { | |
| 53 | t.Fatal(err) | |
| 54 | } | |
| 55 | } | |
| 56 | ||
| 57 | func TestImportACLsTakesRulesFromTheOldRef(t *testing.T) { | |
|
44530e74
Keenan Nemetz
17d ago
|
58 | t.Parallel() |
|
ec5c519c
Keenan Nemetz
17d ago
|
59 | st, acls := withACLs(t) |
| 60 | m, err := st.Create("demo", "01M5OWNER0000000000000000A", "") | |
| 61 | if err != nil { | |
| 62 | t.Fatal(err) | |
| 63 | } | |
| 64 | // Drop the row the creation wrote, so the repository looks pre-upgrade, and put the | |
| 65 | // rules where they used to live. | |
| 66 | if err := acls.Delete(m.ID); err != nil { | |
| 67 | t.Fatal(err) | |
| 68 | } | |
|
c5902451
Keenan Nemetz
17d ago
|
69 | legacy := access.Empty("01M5OWNER0000000000000000A") |
|
ec5c519c
Keenan Nemetz
17d ago
|
70 | if err := legacy.SetEntry("01M5WRITER000000000000000B", "write"); err != nil { |
| 71 | t.Fatal(err) | |
| 72 | } | |
| 73 | legacy.SetRunners([]string{"01M5RUNNER000000000000000C"}) | |
| 74 | legacyWriteACL(t, m, legacy) | |
| 75 | ||
| 76 | imported, err := st.ImportACLs() | |
| 77 | if err != nil { | |
| 78 | t.Fatal(err) | |
| 79 | } | |
| 80 | if imported != 1 { | |
| 81 | t.Fatalf("imported %d repo(s), want 1", imported) | |
| 82 | } | |
| 83 | // The repository the caller is already holding has to see the rules too: the gate | |
| 84 | // reads them from the same store. | |
| 85 | got, err := m.ACL() | |
| 86 | if err != nil { | |
| 87 | t.Fatal(err) | |
| 88 | } | |
|
6c84d387
Keenan Nemetz
16d ago
|
89 | if got.RoleFor("01M5WRITER000000000000000B") != access.Maintain || !got.CanReport("01M5RUNNER000000000000000C") { |
|
ec5c519c
Keenan Nemetz
17d ago
|
90 | t.Fatalf("rules did not survive the import: %+v", got) |
| 91 | } | |
| 92 | ||
| 93 | // Idempotent: a second start takes nothing, and a database row is never overwritten | |
| 94 | // by a stale ref. | |
| 95 | again, err := st.ImportACLs() | |
| 96 | if err != nil || again != 0 { | |
| 97 | t.Fatalf("second import = %d, %v", again, err) | |
| 98 | } | |
| 99 | } | |
| 100 | ||
| 101 | func TestImportACLsLeavesRepositoriesWithoutTheRefAlone(t *testing.T) { | |
|
44530e74
Keenan Nemetz
17d ago
|
102 | t.Parallel() |
|
ec5c519c
Keenan Nemetz
17d ago
|
103 | st, acls := withACLs(t) |
| 104 | m, err := st.Create("demo", "01M5OWNER0000000000000000A", "") | |
| 105 | if err != nil { | |
| 106 | t.Fatal(err) | |
| 107 | } | |
| 108 | if err := acls.Delete(m.ID); err != nil { | |
| 109 | t.Fatal(err) | |
| 110 | } | |
| 111 | if n, err := st.ImportACLs(); err != nil || n != 0 { | |
| 112 | t.Fatalf("imported %d repo(s) from nothing: %v", n, err) | |
| 113 | } | |
| 114 | // No rules is deny-everything, which is what a repository with no ACL ref always | |
| 115 | // meant — and it is why the import has to run before anybody pushes. | |
| 116 | got, err := m.ACL() | |
| 117 | if err != nil { | |
| 118 | t.Fatal(err) | |
| 119 | } | |
|
c5902451
Keenan Nemetz
17d ago
|
120 | if got.RoleFor("01M5OWNER0000000000000000A") != access.None { |
|
ec5c519c
Keenan Nemetz
17d ago
|
121 | t.Fatalf("a repository with no rules granted something: %+v", got) |
| 122 | } | |
| 123 | } | |
| 124 | ||
| 125 | func TestDeletingARepositoryForgetsItsRules(t *testing.T) { | |
|
44530e74
Keenan Nemetz
17d ago
|
126 | t.Parallel() |
|
ec5c519c
Keenan Nemetz
17d ago
|
127 | st, acls := withACLs(t) |
| 128 | m, err := st.Create("demo", "01M5OWNER0000000000000000A", "") | |
| 129 | if err != nil { | |
| 130 | t.Fatal(err) | |
| 131 | } | |
|
b1499c72
Keenan Nemetz
13d ago
|
132 | if err := st.Delete("demo", "01M5OWNER0000000000000000A"); err != nil { |
|
ec5c519c
Keenan Nemetz
17d ago
|
133 | t.Fatal(err) |
| 134 | } | |
| 135 | if got, err := acls.Get(m.ID); err != nil || got != nil { | |
| 136 | t.Fatalf("rules outlived the repository: %+v, %v", got, err) | |
| 137 | } | |
| 138 | } | |
|
1948358f
Keenan Nemetz
15d ago
|
139 | |
| 140 | func TestDescriptionWritesAndClears(t *testing.T) { | |
| 141 | t.Parallel() | |
| 142 | st, _ := withACLs(t) | |
| 143 | m, err := st.Create("demo", "01M5OWNER0000000000000000A", "first") | |
| 144 | if err != nil { | |
| 145 | t.Fatal(err) | |
| 146 | } | |
| 147 | if err := m.SetDescription(" second "); err != nil { | |
| 148 | t.Fatal(err) | |
| 149 | } | |
| 150 | if m.Description != "second" { | |
| 151 | t.Fatalf("in-memory description = %q", m.Description) | |
| 152 | } | |
| 153 | reopened, err := st.Open("demo") | |
| 154 | if err != nil { | |
| 155 | t.Fatal(err) | |
| 156 | } | |
| 157 | if reopened.Description != "second" { | |
| 158 | t.Fatalf("reopened description = %q", reopened.Description) | |
| 159 | } | |
| 160 | if err := m.SetDescription(""); err != nil { | |
| 161 | t.Fatal(err) | |
| 162 | } | |
| 163 | if _, err := os.Stat(filepath.Join(m.Dir, descFile)); !os.IsNotExist(err) { | |
| 164 | t.Fatalf("cleared description left a file: %v", err) | |
| 165 | } | |
| 166 | reopened, err = st.Open("demo") | |
| 167 | if err != nil { | |
| 168 | t.Fatal(err) | |
| 169 | } | |
| 170 | if reopened.Description != "" { | |
| 171 | t.Fatalf("cleared description came back as %q", reopened.Description) | |
| 172 | } | |
| 173 | } | |
|
642e5994
Keenan Nemetz
15d ago
|
174 | |
| 175 | // Public is the other cheap question host policy asks: does this repository have | |
| 176 | // an anonymous reader role? Private repository names must not reach a public | |
| 177 | // certificate log. | |
| 178 | func TestPublicRepositoriesAreTheOnlySitesEligibleForCertificates(t *testing.T) { | |
| 179 | t.Parallel() | |
| 180 | st, acls := withACLs(t) | |
| 181 | if st.Public("missing") { | |
| 182 | t.Fatal("a missing repository is public") | |
| 183 | } | |
| 184 | m, err := st.Create("demo", "01M5OWNER0000000000000000A", "") | |
| 185 | if err != nil { | |
| 186 | t.Fatal(err) | |
| 187 | } | |
| 188 | if st.Public("demo") { | |
| 189 | t.Fatal("a new repository with no default role is public") | |
| 190 | } | |
| 191 | acl, err := m.ACL() | |
| 192 | if err != nil { | |
| 193 | t.Fatal(err) | |
| 194 | } | |
| 195 | acl.Default = "read" | |
| 196 | if err := acls.Set(m.ID, acl, "01M5OWNER0000000000000000A", time.Now()); err != nil { | |
| 197 | t.Fatal(err) | |
| 198 | } | |
| 199 | if !st.Public("demo") { | |
| 200 | t.Fatal("a repository readable by default is not public") | |
| 201 | } | |
| 202 | } |