Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 26m ago 4b8c9e1d
history blame raw 5.4 KiB
1package lfs
2
3// Scoping, which is a security property and therefore needs a test that fails if someone
4// makes the store convenient again.
5//
6// The rule: an object is fetchable only through a repository that has a link to it. The
7// pool is shared, so bytes are still stored once, but knowing a sha256 is no longer
8// enough to read somebody else's artifact.
9
10import (
11 "bytes"
12 "crypto/sha256"
13 "encoding/hex"
14 "errors"
15 "os"
16 "testing"
17)
18
19func oidFor(data []byte) string {
20 sum := sha256.Sum256(data)
21 return hex.EncodeToString(sum[:])
22}
23
24func TestAnObjectIsInvisibleToARepositoryThatDoesNotHoldIt(t *testing.T) {
25 t.Parallel()
26 s := New(t.TempDir(), 1<<20)
27 secret := []byte("an unreleased asset")
28 oid := oidFor(secret)
29
30 if _, _, err := s.Put("private", oid, int64(len(secret)), bytes.NewReader(secret)); err != nil {
31 t.Fatal(err)
32 }
33
34 // The repository that uploaded it can read it.
35 if _, size, err := s.Open("private", oid); err != nil || size != int64(len(secret)) {
36 t.Fatalf("the owning repository cannot read its own object: %v", err)
37 }
38 // Any other repository cannot, even though the bytes are in the same store and the
39 // id is known exactly.
40 for _, other := range []string{"public", ""} {
41 if other == "" {
42 continue
43 }
44 if s.Has(other, oid) {
45 t.Fatalf("%q can see an object it was never given", other)
46 }
47 if _, _, err := s.Open(other, oid); !errors.Is(err, ErrNotFound) {
48 t.Fatalf("%q read another repository's object: %v", other, err)
49 }
50 if _, err := s.Stat(other, oid); !errors.Is(err, ErrNotFound) {
51 t.Fatalf("%q stat'd another repository's object: %v", other, err)
52 }
53 }
54}
55
56// Two repositories may hold the same bytes. That is one copy on disk and two links, so
57// isolation does not cost deduplication.
58func TestTwoRepositoriesShareOneCopyOfTheBytes(t *testing.T) {
59 t.Parallel()
60 root := t.TempDir()
61 s := New(root, 1<<20)
62 artifact := []byte("a release artifact both projects use")
63 oid := oidFor(artifact)
64
65 if _, created, err := s.Put("alpha", oid, int64(len(artifact)), bytes.NewReader(artifact)); err != nil || !created {
66 t.Fatalf("first put: created=%v err=%v", created, err)
67 }
68 // The second repository uploads the same bytes: no new storage, but a link, and
69 // `created` speaks for *this* repository rather than for the pool.
70 if _, created, err := s.Put("beta", oid, int64(len(artifact)), bytes.NewReader(artifact)); err != nil || !created {
71 t.Fatalf("second put: created=%v err=%v", created, err)
72 }
73 if _, created, err := s.Put("beta", oid, int64(len(artifact)), bytes.NewReader(artifact)); err != nil || created {
74 t.Fatalf("re-putting the same object reported created=%v", created)
75 }
76
77 pool, err := os.Stat(s.ObjectPath(oid))
78 if err != nil {
79 t.Fatal(err)
80 }
81 link, err := os.Stat(s.RepoPath("beta", oid))
82 if err != nil {
83 t.Fatal(err)
84 }
85 if !os.SameFile(pool, link) {
86 t.Fatal("the second repository copied the bytes instead of linking them")
87 }
88
89 // Dropping one repository's link leaves the other's data intact.
90 if err := s.Unlink("alpha", oid); err != nil {
91 t.Fatal(err)
92 }
93 if s.Has("alpha", oid) {
94 t.Fatal("alpha still holds the object after being unlinked")
95 }
96 if !s.Has("beta", oid) {
97 t.Fatal("unlinking alpha removed beta's object")
98 }
99}
100
101// A hard link is the mechanism, so a rename of the repository has to move the namespace
102// with it or every fetch 404s.
103func TestRenamingARepositoryMovesItsNamespace(t *testing.T) {
104 t.Parallel()
105 s := New(t.TempDir(), 1<<20)
106 data := []byte("bytes")
107 oid := oidFor(data)
108 if _, _, err := s.Put("before", oid, int64(len(data)), bytes.NewReader(data)); err != nil {
109 t.Fatal(err)
110 }
111 if err := s.RenameRepo("before", "after"); err != nil {
112 t.Fatal(err)
113 }
114 if !s.Has("after", oid) {
115 t.Fatal("the renamed repository cannot fetch its own object")
116 }
117 if s.Has("before", oid) {
118 t.Fatal("the old name still serves the object")
119 }
120 // Renaming a repository with no objects is not an error.
121 if err := s.RenameRepo("nothing-here", "nor-here"); err != nil {
122 t.Fatalf("renaming an empty repository failed: %v", err)
123 }
124}
125
126// A repository name is a directory name, so a path that could escape the namespace is
127// refused rather than creating something outside the store.
128func TestRepositoryNamesCannotEscapeTheNamespace(t *testing.T) {
129 t.Parallel()
130 s := New(t.TempDir(), 1<<20)
131 data := []byte("bytes")
132 oid := oidFor(data)
133 for _, bad := range []string{"", ".", "..", "a/b", "a\x00b"} {
134 if _, _, err := s.Put(bad, oid, int64(len(data)), bytes.NewReader(data)); err == nil {
135 t.Fatalf("%q was accepted as a repository name", bad)
136 }
137 if err := s.Link(bad, oid); err == nil {
138 t.Fatalf("Link accepted %q", bad)
139 }
140 }
141}
142
143// Remove means gone: it takes the pool's bytes and every repository's link, because a
144// sweep that reported an object removed while leaving something fetchable is worse than
145// one that failed.
146func TestRemoveTakesEveryLink(t *testing.T) {
147 t.Parallel()
148 s := New(t.TempDir(), 1<<20)
149 data := []byte("bytes")
150 oid := oidFor(data)
151 for _, repo := range []string{"one", "two"} {
152 if _, _, err := s.Put(repo, oid, int64(len(data)), bytes.NewReader(data)); err != nil {
153 t.Fatal(err)
154 }
155 }
156 if err := s.Remove(oid); err != nil {
157 t.Fatal(err)
158 }
159 for _, repo := range []string{"one", "two"} {
160 if s.Has(repo, oid) {
161 t.Fatalf("%s can still fetch a removed object", repo)
162 }
163 }
164 if _, err := os.Stat(s.ObjectPath(oid)); !errors.Is(err, os.ErrNotExist) {
165 t.Fatalf("the pool still holds a removed object: %v", err)
166 }
167}
168