| 1 | package docs |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "testing" |
| 6 | ) |
| 7 | |
| 8 | // readable is the seam sharing adds beside owned. owned answers "whose |
| 9 | // document is this"; readable answers "who may read it", which is the owner |
| 10 | // plus anybody holding a grant. |
| 11 | // |
| 12 | // owned's own doc comment says the day sharing lands is the day this changes, |
| 13 | // and TestOwnedScopesByOwner was written to be the test that has to keep |
| 14 | // passing when it does. It still tests owned, unchanged, and owned still means |
| 15 | // exactly what it meant. What is new is a second function beside it, so no |
| 16 | // call site that genuinely wants ownership silently starts accepting grantees. |
| 17 | func TestReadableIncludesGrantees(t *testing.T) { |
| 18 | app, err := NewForTest(t) |
| 19 | if err != nil { |
| 20 | t.Fatalf("NewForTest: %v", err) |
| 21 | } |
| 22 | ctx := context.Background() |
| 23 | store := app.Store() |
| 24 | |
| 25 | alice, err := store.EnsurePerson(ctx, "alice@example.com") |
| 26 | if err != nil { |
| 27 | t.Fatalf("EnsurePerson(alice): %v", err) |
| 28 | } |
| 29 | bob, err := store.EnsurePerson(ctx, "bob@example.com") |
| 30 | if err != nil { |
| 31 | t.Fatalf("EnsurePerson(bob): %v", err) |
| 32 | } |
| 33 | carol, err := store.EnsurePerson(ctx, "carol@example.com") |
| 34 | if err != nil { |
| 35 | t.Fatalf("EnsurePerson(carol): %v", err) |
| 36 | } |
| 37 | |
| 38 | doc, err := store.Create(ctx, alice, "Alice's document") |
| 39 | if err != nil { |
| 40 | t.Fatalf("Create: %v", err) |
| 41 | } |
| 42 | if err := store.Grant(ctx, alice, doc.Sqid, "bob@example.com", RoleReader); err != nil { |
| 43 | t.Fatalf("Grant: %v", err) |
| 44 | } |
| 45 | |
| 46 | // Bob reads it because he was granted it. |
| 47 | var bobRows []*Document |
| 48 | if err := readable(store.db.WithContext(ctx), localActor(bob, "bob@example.com")).Find(&bobRows).Error; err != nil { |
| 49 | t.Fatalf("readable(bob): %v", err) |
| 50 | } |
| 51 | if len(bobRows) != 1 || bobRows[0].ID != doc.ID { |
| 52 | t.Errorf("readable(bob) = %d row(s), want exactly Alice's document", len(bobRows)) |
| 53 | } |
| 54 | |
| 55 | // Alice still reads her own. |
| 56 | var aliceRows []*Document |
| 57 | if err := readable(store.db.WithContext(ctx), localActor(alice, "alice@example.com")).Find(&aliceRows).Error; err != nil { |
| 58 | t.Fatalf("readable(alice): %v", err) |
| 59 | } |
| 60 | if len(aliceRows) != 1 { |
| 61 | t.Errorf("readable(alice) = %d row(s), want 1", len(aliceRows)) |
| 62 | } |
| 63 | |
| 64 | // Carol was granted nothing and reads nothing. Without this the whole |
| 65 | // thing could be a query that returns every row. |
| 66 | var carolRows []*Document |
| 67 | if err := readable(store.db.WithContext(ctx), localActor(carol, "carol@example.com")).Find(&carolRows).Error; err != nil { |
| 68 | t.Fatalf("readable(carol): %v", err) |
| 69 | } |
| 70 | if len(carolRows) != 0 { |
| 71 | t.Errorf("readable(carol) = %d row(s), want 0 — carol holds no grant", len(carolRows)) |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | // A grant is keyed on the address, not on a people row, so it can be made to |
| 76 | // somebody who has never signed in. EnsurePerson seeds a sample library on the |
| 77 | // create branch, so minting a Person for an invitee would hand them six sample |
| 78 | // documents they never asked for. |
| 79 | func TestGrantToSomebodyWhoHasNeverSignedIn(t *testing.T) { |
| 80 | app, err := NewForTest(t) |
| 81 | if err != nil { |
| 82 | t.Fatalf("NewForTest: %v", err) |
| 83 | } |
| 84 | ctx := context.Background() |
| 85 | store := app.Store() |
| 86 | |
| 87 | alice, err := store.EnsurePerson(ctx, "alice@example.com") |
| 88 | if err != nil { |
| 89 | t.Fatalf("EnsurePerson(alice): %v", err) |
| 90 | } |
| 91 | doc, err := store.Create(ctx, alice, "Alice's document") |
| 92 | if err != nil { |
| 93 | t.Fatalf("Create: %v", err) |
| 94 | } |
| 95 | |
| 96 | if err := store.Grant(ctx, alice, doc.Sqid, "dave@example.com", RoleReader); err != nil { |
| 97 | t.Fatalf("Grant to a stranger: %v", err) |
| 98 | } |
| 99 | |
| 100 | var people []*Person |
| 101 | if err := store.db.WithContext(ctx).Where("email = ?", "dave@example.com").Find(&people).Error; err != nil { |
| 102 | t.Fatalf("counting people: %v", err) |
| 103 | } |
| 104 | if len(people) != 0 { |
| 105 | t.Errorf("granting minted a Person row for dave, which would seed him a sample library") |
| 106 | } |
| 107 | |
| 108 | // And the grant works the moment he does sign in. |
| 109 | dave, err := store.EnsurePerson(ctx, "dave@example.com") |
| 110 | if err != nil { |
| 111 | t.Fatalf("EnsurePerson(dave): %v", err) |
| 112 | } |
| 113 | var daveRows []*Document |
| 114 | if err := readable(store.db.WithContext(ctx), localActor(dave, "dave@example.com")).Find(&daveRows).Error; err != nil { |
| 115 | t.Fatalf("readable(dave): %v", err) |
| 116 | } |
| 117 | if len(daveRows) != 1 { |
| 118 | t.Errorf("readable(dave) = %d row(s), want the document granted before he existed", len(daveRows)) |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | // Addresses are matched the way EnsurePerson stores them: lowercased and |
| 123 | // trimmed. A grant made to "Bob@Example.com " must be the grant Bob holds. |
| 124 | func TestGrantSubjectIsNormalised(t *testing.T) { |
| 125 | app, err := NewForTest(t) |
| 126 | if err != nil { |
| 127 | t.Fatalf("NewForTest: %v", err) |
| 128 | } |
| 129 | ctx := context.Background() |
| 130 | store := app.Store() |
| 131 | |
| 132 | alice, _ := store.EnsurePerson(ctx, "alice@example.com") |
| 133 | bob, _ := store.EnsurePerson(ctx, "bob@example.com") |
| 134 | doc, err := store.Create(ctx, alice, "Alice's document") |
| 135 | if err != nil { |
| 136 | t.Fatalf("Create: %v", err) |
| 137 | } |
| 138 | |
| 139 | if err := store.Grant(ctx, alice, doc.Sqid, " Bob@Example.COM ", RoleReader); err != nil { |
| 140 | t.Fatalf("Grant: %v", err) |
| 141 | } |
| 142 | |
| 143 | var rows []*Document |
| 144 | if err := readable(store.db.WithContext(ctx), localActor(bob, "bob@example.com")).Find(&rows).Error; err != nil { |
| 145 | t.Fatalf("readable(bob): %v", err) |
| 146 | } |
| 147 | if len(rows) != 1 { |
| 148 | t.Errorf("readable(bob) = %d row(s); the grant's address was not normalised", len(rows)) |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | // An orphaned document — owner_id 0, which 0002_owners.sql writes onto every |
| 153 | // pre-existing row — stays invisible even if a grant points at it. Three tests |
| 154 | // already pin that owner 0 matches nothing; a grants clause must not become |
| 155 | // the way around them. |
| 156 | func TestReadableKeepsOrphansInvisible(t *testing.T) { |
| 157 | app, err := NewForTest(t) |
| 158 | if err != nil { |
| 159 | t.Fatalf("NewForTest: %v", err) |
| 160 | } |
| 161 | ctx := context.Background() |
| 162 | store := app.Store() |
| 163 | |
| 164 | alice, _ := store.EnsurePerson(ctx, "alice@example.com") |
| 165 | bob, _ := store.EnsurePerson(ctx, "bob@example.com") |
| 166 | doc, err := store.Create(ctx, alice, "Alice's document") |
| 167 | if err != nil { |
| 168 | t.Fatalf("Create: %v", err) |
| 169 | } |
| 170 | if err := store.Grant(ctx, alice, doc.Sqid, "bob@example.com", RoleReader); err != nil { |
| 171 | t.Fatalf("Grant: %v", err) |
| 172 | } |
| 173 | |
| 174 | // Orphan it after the grant exists. |
| 175 | if err := store.db.WithContext(ctx).Model(&Document{}).Where("id = ?", doc.ID). |
| 176 | Update("owner_id", 0).Error; err != nil { |
| 177 | t.Fatalf("orphaning: %v", err) |
| 178 | } |
| 179 | |
| 180 | var rows []*Document |
| 181 | if err := readable(store.db.WithContext(ctx), localActor(bob, "bob@example.com")).Find(&rows).Error; err != nil { |
| 182 | t.Fatalf("readable(bob): %v", err) |
| 183 | } |
| 184 | if len(rows) != 0 { |
| 185 | t.Errorf("an orphaned document was readable through a grant; owner 0 must match nothing") |
| 186 | } |
| 187 | } |
| 188 | |