Go · 5499 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package protocol_test
4
5 import (
6 "context"
7 "errors"
8 "testing"
9
10 "github.com/tenseleyFlow/shithub/internal/auth/policy"
11 policydb "github.com/tenseleyFlow/shithub/internal/auth/policy/sqlc"
12 "github.com/tenseleyFlow/shithub/internal/git/protocol"
13 )
14
15 // TestDispatch_CollabWriteCanPushPrivate confirms that adding eve as a
16 // 'write' collaborator on alice's private repo lets her push. Before
17 // S15 this would have been ErrSSHPermDenied; after S15 the policy
18 // package grants based on the role row.
19 func TestDispatch_CollabWriteCanPushPrivate(t *testing.T) {
20 t.Parallel()
21 env := setupDispatch(t)
22 pq := policydb.New()
23 if err := pq.UpsertCollabRole(context.Background(), env.pool, policydb.UpsertCollabRoleParams{
24 RepoID: repoIDFor(t, env, "alice", "private"),
25 UserID: env.eve,
26 Role: policydb.CollabRoleWrite,
27 }); err != nil {
28 t.Fatalf("UpsertCollabRole: %v", err)
29 }
30
31 _, _, err := protocol.PrepareDispatch(context.Background(), env.deps, protocol.SSHDispatchInput{
32 OriginalCommand: "git-receive-pack 'alice/private'",
33 UserID: env.eve,
34 })
35 if err != nil {
36 t.Fatalf("collab-write push denied: %v", err)
37 }
38 }
39
40 // TestDispatch_CollabReadCannotPush confirms that a 'read' grant lets
41 // eve clone but not push.
42 func TestDispatch_CollabReadCannotPush(t *testing.T) {
43 t.Parallel()
44 env := setupDispatch(t)
45 pq := policydb.New()
46 if err := pq.UpsertCollabRole(context.Background(), env.pool, policydb.UpsertCollabRoleParams{
47 RepoID: repoIDFor(t, env, "alice", "private"),
48 UserID: env.eve,
49 Role: policydb.CollabRoleRead,
50 }); err != nil {
51 t.Fatalf("UpsertCollabRole: %v", err)
52 }
53 // Pull: allowed.
54 if _, _, err := protocol.PrepareDispatch(context.Background(), env.deps, protocol.SSHDispatchInput{
55 OriginalCommand: "git-upload-pack 'alice/private'",
56 UserID: env.eve,
57 }); err != nil {
58 t.Fatalf("collab-read pull denied: %v", err)
59 }
60 // Push: denied with permission-denied (not 404, eve has read so
61 // existence is no secret).
62 _, _, err := protocol.PrepareDispatch(context.Background(), env.deps, protocol.SSHDispatchInput{
63 OriginalCommand: "git-receive-pack 'alice/private'",
64 UserID: env.eve,
65 })
66 if !errors.Is(err, protocol.ErrSSHPermDenied) {
67 t.Fatalf("collab-read push: err = %v, want ErrSSHPermDenied", err)
68 }
69 }
70
71 // TestDispatch_DemotedCollabLosesAccessNextRequest mirrors the spec's
72 // definition-of-done bullet: dropping the role takes effect immediately
73 // on the next request (no cross-request cache).
74 func TestDispatch_DemotedCollabLosesAccessNextRequest(t *testing.T) {
75 t.Parallel()
76 env := setupDispatch(t)
77 pq := policydb.New()
78 repoID := repoIDFor(t, env, "alice", "private")
79 if err := pq.UpsertCollabRole(context.Background(), env.pool, policydb.UpsertCollabRoleParams{
80 RepoID: repoID, UserID: env.eve, Role: policydb.CollabRoleAdmin,
81 }); err != nil {
82 t.Fatalf("UpsertCollabRole admin: %v", err)
83 }
84 // Eve as admin: push allowed.
85 if _, _, err := protocol.PrepareDispatch(context.Background(), env.deps, protocol.SSHDispatchInput{
86 OriginalCommand: "git-receive-pack 'alice/private'",
87 UserID: env.eve,
88 }); err != nil {
89 t.Fatalf("admin push denied: %v", err)
90 }
91 // Demote to read.
92 if err := pq.UpsertCollabRole(context.Background(), env.pool, policydb.UpsertCollabRoleParams{
93 RepoID: repoID, UserID: env.eve, Role: policydb.CollabRoleRead,
94 }); err != nil {
95 t.Fatalf("UpsertCollabRole read: %v", err)
96 }
97 // Next request: push denied.
98 _, _, err := protocol.PrepareDispatch(context.Background(), env.deps, protocol.SSHDispatchInput{
99 OriginalCommand: "git-receive-pack 'alice/private'",
100 UserID: env.eve,
101 })
102 if !errors.Is(err, protocol.ErrSSHPermDenied) {
103 t.Fatalf("demoted push: err = %v, want ErrSSHPermDenied", err)
104 }
105 }
106
107 // TestDispatch_RemovedCollabFallsBack404 confirms that fully removing
108 // the row turns eve back into a stranger on a private repo: the
109 // rejection is "repo not found" (existence-leak guard).
110 func TestDispatch_RemovedCollabFallsBack404(t *testing.T) {
111 t.Parallel()
112 env := setupDispatch(t)
113 pq := policydb.New()
114 repoID := repoIDFor(t, env, "alice", "private")
115 if err := pq.UpsertCollabRole(context.Background(), env.pool, policydb.UpsertCollabRoleParams{
116 RepoID: repoID, UserID: env.eve, Role: policydb.CollabRoleRead,
117 }); err != nil {
118 t.Fatalf("UpsertCollabRole: %v", err)
119 }
120 if err := pq.RemoveCollab(context.Background(), env.pool, policydb.RemoveCollabParams{
121 RepoID: repoID, UserID: env.eve,
122 }); err != nil {
123 t.Fatalf("RemoveCollab: %v", err)
124 }
125 _, _, err := protocol.PrepareDispatch(context.Background(), env.deps, protocol.SSHDispatchInput{
126 OriginalCommand: "git-upload-pack 'alice/private'",
127 UserID: env.eve,
128 })
129 if !errors.Is(err, protocol.ErrSSHRepoNotFound) {
130 t.Fatalf("removed collab pull: err = %v, want ErrSSHRepoNotFound", err)
131 }
132 // Sanity-check the policy decision separately.
133 _ = policy.ActionRepoRead
134 }
135
136 // repoIDFor looks up the bigserial id for a repo so the collab insert
137 // references a real row. setupDispatch creates "public" and "private";
138 // callers ask by name.
139 func repoIDFor(t *testing.T, env *dispatchEnv, owner, name string) int64 {
140 t.Helper()
141 var id int64
142 err := env.pool.QueryRow(context.Background(),
143 `SELECT r.id FROM repos r JOIN users u ON u.id = r.owner_user_id
144 WHERE u.username = $1 AND r.name = $2`,
145 owner, name).Scan(&id)
146 if err != nil {
147 t.Fatalf("repoIDFor %s/%s: %v", owner, name, err)
148 }
149 return id
150 }
151