| 1 | package docs |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "testing" |
| 7 | ) |
| 8 | |
| 9 | // The write path has to authorize the *actor*, not the document. |
| 10 | // |
| 11 | // Before sharing, ApplyUpdate verified `owned(db, doc.OwnerID).Where("id = ?", |
| 12 | // doc.ID)` — the document against its own owner, which is true of every |
| 13 | // document by construction. That was harmless while the only way to hold a |
| 14 | // *Document was to have fetched it through an owner-scoped query, so the |
| 15 | // check was a belt on top of the read scope's braces. The moment reads accept |
| 16 | // grantees it stops being harmless: a reader who can fetch the document can |
| 17 | // satisfy a check that only ever asked whether the document is its own |
| 18 | // owner's. |
| 19 | // |
| 20 | // store.go's own comment records this class of bug being fixed twice already. |
| 21 | // This is the third, caught before it shipped rather than after. |
| 22 | func TestApplyUpdateRefusesAReader(t *testing.T) { |
| 23 | app, err := NewForTest(t) |
| 24 | if err != nil { |
| 25 | t.Fatalf("NewForTest: %v", err) |
| 26 | } |
| 27 | ctx := context.Background() |
| 28 | store := app.Store() |
| 29 | |
| 30 | alice, _ := store.EnsurePerson(ctx, "alice@example.com") |
| 31 | bob, _ := store.EnsurePerson(ctx, "bob@example.com") |
| 32 | |
| 33 | doc, err := store.Create(ctx, alice, "Alice's document") |
| 34 | if err != nil { |
| 35 | t.Fatalf("Create: %v", err) |
| 36 | } |
| 37 | if err := store.Grant(ctx, alice, doc.Sqid, "bob@example.com", RoleReader); err != nil { |
| 38 | t.Fatalf("Grant: %v", err) |
| 39 | } |
| 40 | |
| 41 | // Bob can read it — that is what the grant is for. |
| 42 | var rows []*Document |
| 43 | if err := readable(store.db.WithContext(ctx), localActor(bob, "bob@example.com")).Find(&rows).Error; err != nil { |
| 44 | t.Fatalf("readable(bob): %v", err) |
| 45 | } |
| 46 | if len(rows) != 1 { |
| 47 | t.Fatalf("bob cannot read the document he was granted; the rest of this test proves nothing") |
| 48 | } |
| 49 | |
| 50 | // And he cannot write it. |
| 51 | _, err = store.ApplyUpdate(ctx, doc, localActor(bob, "bob@example.com"), []byte{0}) |
| 52 | if !errors.Is(err, ErrNotWritable) { |
| 53 | t.Errorf("ApplyUpdate as a reader: got %v, want ErrNotWritable", err) |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | // A writer grant does let them write, or the role is decoration. |
| 58 | func TestApplyUpdateAllowsAWriter(t *testing.T) { |
| 59 | app, err := NewForTest(t) |
| 60 | if err != nil { |
| 61 | t.Fatalf("NewForTest: %v", err) |
| 62 | } |
| 63 | ctx := context.Background() |
| 64 | store := app.Store() |
| 65 | |
| 66 | alice, _ := store.EnsurePerson(ctx, "alice@example.com") |
| 67 | bob, _ := store.EnsurePerson(ctx, "bob@example.com") |
| 68 | |
| 69 | doc, err := store.Create(ctx, alice, "Alice's document") |
| 70 | if err != nil { |
| 71 | t.Fatalf("Create: %v", err) |
| 72 | } |
| 73 | if err := store.Grant(ctx, alice, doc.Sqid, "bob@example.com", RoleWriter); err != nil { |
| 74 | t.Fatalf("Grant: %v", err) |
| 75 | } |
| 76 | |
| 77 | // A malformed CRDT update is fine here: it must get *past* authorization |
| 78 | // and fail on its own merits, which is what distinguishes "refused" from |
| 79 | // "not a valid update". |
| 80 | _, err = store.ApplyUpdate(ctx, doc, localActor(bob, "bob@example.com"), []byte{0}) |
| 81 | if errors.Is(err, ErrNotWritable) { |
| 82 | t.Errorf("ApplyUpdate as a writer was refused authorization; the grant's role is doing nothing") |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | // The owner keeps writing without any grant to themselves. |
| 87 | func TestApplyUpdateAllowsTheOwner(t *testing.T) { |
| 88 | app, err := NewForTest(t) |
| 89 | if err != nil { |
| 90 | t.Fatalf("NewForTest: %v", err) |
| 91 | } |
| 92 | ctx := context.Background() |
| 93 | store := app.Store() |
| 94 | |
| 95 | alice, _ := store.EnsurePerson(ctx, "alice@example.com") |
| 96 | doc, err := store.Create(ctx, alice, "Alice's document") |
| 97 | if err != nil { |
| 98 | t.Fatalf("Create: %v", err) |
| 99 | } |
| 100 | |
| 101 | _, err = store.ApplyUpdate(ctx, doc, localActor(alice, "alice@example.com"), []byte{0}) |
| 102 | if errors.Is(err, ErrNotWritable) { |
| 103 | t.Errorf("the owner was refused a write to her own document: %v", err) |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | // A stranger holding neither ownership nor a grant is refused, and refused the |
| 108 | // same way a reader is — the write path does not tell them which. |
| 109 | func TestApplyUpdateRefusesAStranger(t *testing.T) { |
| 110 | app, err := NewForTest(t) |
| 111 | if err != nil { |
| 112 | t.Fatalf("NewForTest: %v", err) |
| 113 | } |
| 114 | ctx := context.Background() |
| 115 | store := app.Store() |
| 116 | |
| 117 | alice, _ := store.EnsurePerson(ctx, "alice@example.com") |
| 118 | carol, _ := store.EnsurePerson(ctx, "carol@example.com") |
| 119 | doc, err := store.Create(ctx, alice, "Alice's document") |
| 120 | if err != nil { |
| 121 | t.Fatalf("Create: %v", err) |
| 122 | } |
| 123 | |
| 124 | _, err = store.ApplyUpdate(ctx, doc, localActor(carol, "carol@example.com"), []byte{0}) |
| 125 | if !errors.Is(err, ErrNotWritable) { |
| 126 | t.Errorf("ApplyUpdate as a stranger: got %v, want ErrNotWritable", err) |
| 127 | } |
| 128 | } |
| 129 | |