+14
-5
appview/pages/pages.go
+14
-5
appview/pages/pages.go
···
152
}
153
154
type RepoInfo struct {
155
-
Name string
156
-
OwnerDid string
157
-
OwnerHandle string
158
-
Description string
159
}
160
161
func (r RepoInfo) OwnerWithAt() string {
···
177
}
178
179
func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error {
180
-
181
return p.executeRepo("repo/index", w, params)
182
}
183
···
239
240
func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {
241
return p.executeRepo("repo/blob", w, params)
242
}
243
244
func (p *Pages) Static() http.Handler {
···
152
}
153
154
type RepoInfo struct {
155
+
Name string
156
+
OwnerDid string
157
+
OwnerHandle string
158
+
Description string
159
+
SettingsAllowed bool
160
}
161
162
func (r RepoInfo) OwnerWithAt() string {
···
178
}
179
180
func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error {
181
return p.executeRepo("repo/index", w, params)
182
}
183
···
239
240
func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {
241
return p.executeRepo("repo/blob", w, params)
242
+
}
243
+
244
+
type RepoSettingsParams struct {
245
+
LoggedInUser *auth.User
246
+
Collaborators [][]string
247
+
}
248
+
249
+
func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error {
250
+
return p.executeRepo("repo/settings", w, params)
251
}
252
253
func (p *Pages) Static() http.Handler {
+23
-23
appview/pages/templates/layouts/repobase.html
+23
-23
appview/pages/templates/layouts/repobase.html
···
2
3
{{ define "content" }}
4
5
-
<div id="repo-header">
6
-
<h1>{{ .RepoInfo.FullName }}</h1>
7
-
{{ if .RepoInfo.Description }}
8
-
<h3 class="desc">{{ .RepoInfo.Description }}</h3>
9
-
{{ else }}
10
-
<em>this repo has no description</em>
11
-
</div>
12
-
13
-
{{ with .IsEmpty }}
14
-
{{ else }}
15
-
<div id="repo-links">
16
-
<nav>
17
-
<a href="/{{ .RepoInfo.FullName }}">summary</a> ·
18
-
<a href="/{{ .RepoInfo.FullName }}/branches">branches</a> ·
19
-
<a href="/{{ .RepoInfo.FullName }}/tags">tags</a>
20
-
</nav>
21
-
<div>
22
-
{{ end }}
23
24
-
{{ end }}
25
26
-
{{ block "repoContent" . }} {{ end }}
27
28
{{ end }}
29
30
{{ define "layouts/repobase" }}
31
-
32
-
{{ template "layouts/base" . }}
33
-
34
{{ end }}
···
2
3
{{ define "content" }}
4
5
+
<div id="repo-header">
6
+
<h1>{{ .RepoInfo.FullName }}</h1>
7
+
{{ if .RepoInfo.Description }}
8
+
<h3 class="desc">{{ .RepoInfo.Description }}</h3>
9
+
{{ else }}
10
+
<em>this repo has no description</em>
11
+
{{ end }}
12
+
</div>
13
14
+
{{ with .IsEmpty }}
15
+
{{ else }}
16
+
<div id="repo-links">
17
+
<nav>
18
+
<a href="/{{ .RepoInfo.FullName }}">summary</a> ·
19
+
<a href="/{{ .RepoInfo.FullName }}/branches">branches</a> ·
20
+
<a href="/{{ .RepoInfo.FullName }}/tags">tags</a>
21
+
{{ if .RepoInfo.SettingsAllowed }}
22
+
· <a href="/{{ .RepoInfo.FullName }}/settings">settings</a>
23
+
{{ end }}
24
+
</nav>
25
+
<div>
26
+
{{ end }}
27
28
+
{{ block "repoContent" . }} {{ end }}
29
30
{{ end }}
31
32
{{ define "layouts/repobase" }}
33
+
{{ template "layouts/base" . }}
34
{{ end }}
+5
-1
appview/pages/templates/repo/branches.html
+5
-1
appview/pages/templates/repo/branches.html
+6
appview/pages/templates/repo/settings.html
+6
appview/pages/templates/repo/settings.html
+30
appview/state/middleware.go
+30
appview/state/middleware.go
···
104
}
105
}
106
107
+
func RepoPermissionMiddleware(s *State, requiredPerm string) Middleware {
108
+
return func(next http.Handler) http.Handler {
109
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
110
+
// requires auth also
111
+
actor := s.auth.GetUser(r)
112
+
if actor == nil {
113
+
// we need a logged in user
114
+
log.Printf("not logged in, redirecting")
115
+
http.Error(w, "Forbiden", http.StatusUnauthorized)
116
+
return
117
+
}
118
+
f, err := fullyResolvedRepo(r)
119
+
if err != nil {
120
+
http.Error(w, "malformed url", http.StatusBadRequest)
121
+
return
122
+
}
123
+
124
+
ok, err := s.enforcer.E.Enforce(actor.Did, f.Knot, f.OwnerSlashRepo(), requiredPerm)
125
+
if err != nil || !ok {
126
+
// we need a logged in user
127
+
log.Printf("%s does not have perms of a %s in repo %s", actor.Did, requiredPerm, f.OwnerSlashRepo())
128
+
http.Error(w, "Forbiden", http.StatusUnauthorized)
129
+
return
130
+
}
131
+
132
+
next.ServeHTTP(w, r)
133
+
})
134
+
}
135
+
}
136
+
137
func StripLeadingAt(next http.Handler) http.Handler {
138
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
139
path := req.URL.Path
+179
-50
appview/state/repo.go
+179
-50
appview/state/repo.go
···
6
"io"
7
"log"
8
"net/http"
9
10
"github.com/bluesky-social/indigo/atproto/identity"
11
"github.com/go-chi/chi/v5"
12
"github.com/sotangled/tangled/appview/pages"
13
"github.com/sotangled/tangled/types"
14
)
15
16
func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) {
17
-
repoName, knot, id, err := repoKnotAndId(r)
18
if err != nil {
19
-
log.Println("failed to get repo and knot", err)
20
return
21
}
22
23
-
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s", knot, id.DID.String(), repoName))
24
if err != nil {
25
log.Println("failed to reach knotserver", err)
26
return
···
42
43
log.Println(resp.Status, result)
44
45
s.pages.RepoIndexPage(w, pages.RepoIndexParams{
46
-
LoggedInUser: s.auth.GetUser(r),
47
RepoInfo: pages.RepoInfo{
48
-
OwnerDid: id.DID.String(),
49
-
OwnerHandle: id.Handle.String(),
50
-
Name: repoName,
51
},
52
RepoIndexResponse: result,
53
})
···
56
}
57
58
func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) {
59
-
repoName, knot, id, err := repoKnotAndId(r)
60
if err != nil {
61
-
log.Println("failed to get repo and knot", err)
62
return
63
}
64
65
ref := chi.URLParam(r, "ref")
66
-
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/log/%s", knot, id.DID.String(), repoName, ref))
67
if err != nil {
68
log.Println("failed to reach knotserver", err)
69
return
···
82
return
83
}
84
85
s.pages.RepoLog(w, pages.RepoLogParams{
86
-
LoggedInUser: s.auth.GetUser(r),
87
RepoInfo: pages.RepoInfo{
88
-
OwnerDid: id.DID.String(),
89
-
OwnerHandle: id.Handle.String(),
90
-
Name: repoName,
91
},
92
RepoLogResponse: result,
93
})
···
95
}
96
97
func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) {
98
-
repoName, knot, id, err := repoKnotAndId(r)
99
if err != nil {
100
-
log.Println("failed to get repo and knot", err)
101
return
102
}
103
104
ref := chi.URLParam(r, "ref")
105
-
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/commit/%s", knot, id.DID.String(), repoName, ref))
106
if err != nil {
107
log.Println("failed to reach knotserver", err)
108
return
···
121
return
122
}
123
124
s.pages.RepoCommit(w, pages.RepoCommitParams{
125
-
LoggedInUser: s.auth.GetUser(r),
126
RepoInfo: pages.RepoInfo{
127
-
OwnerDid: id.DID.String(),
128
-
OwnerHandle: id.Handle.String(),
129
-
Name: repoName,
130
},
131
RepoCommitResponse: result,
132
})
···
134
}
135
136
func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) {
137
-
repoName, knot, id, err := repoKnotAndId(r)
138
if err != nil {
139
-
log.Println("failed to get repo and knot", err)
140
return
141
}
142
143
ref := chi.URLParam(r, "ref")
144
treePath := chi.URLParam(r, "*")
145
-
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/tree/%s/%s", knot, id.DID.String(), repoName, ref, treePath))
146
if err != nil {
147
log.Println("failed to reach knotserver", err)
148
return
···
163
164
log.Println(result)
165
166
s.pages.RepoTree(w, pages.RepoTreeParams{
167
-
LoggedInUser: s.auth.GetUser(r),
168
RepoInfo: pages.RepoInfo{
169
-
OwnerDid: id.DID.String(),
170
-
OwnerHandle: id.Handle.String(),
171
-
Name: repoName,
172
},
173
RepoTreeResponse: result,
174
})
···
176
}
177
178
func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) {
179
-
repoName, knot, id, err := repoKnotAndId(r)
180
if err != nil {
181
log.Println("failed to get repo and knot", err)
182
return
183
}
184
185
-
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/tags", knot, id.DID.String(), repoName))
186
if err != nil {
187
log.Println("failed to reach knotserver", err)
188
return
···
201
return
202
}
203
204
s.pages.RepoTags(w, pages.RepoTagsParams{
205
-
LoggedInUser: s.auth.GetUser(r),
206
RepoInfo: pages.RepoInfo{
207
-
OwnerDid: id.DID.String(),
208
-
OwnerHandle: id.Handle.String(),
209
-
Name: repoName,
210
},
211
RepoTagsResponse: result,
212
})
···
214
}
215
216
func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) {
217
-
repoName, knot, id, err := repoKnotAndId(r)
218
if err != nil {
219
log.Println("failed to get repo and knot", err)
220
return
221
}
222
223
-
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/branches", knot, id.DID.String(), repoName))
224
if err != nil {
225
log.Println("failed to reach knotserver", err)
226
return
···
239
return
240
}
241
242
s.pages.RepoBranches(w, pages.RepoBranchesParams{
243
-
LoggedInUser: s.auth.GetUser(r),
244
RepoInfo: pages.RepoInfo{
245
-
OwnerDid: id.DID.String(),
246
-
OwnerHandle: id.Handle.String(),
247
-
Name: repoName,
248
},
249
RepoBranchesResponse: result,
250
})
···
252
}
253
254
func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) {
255
-
repoName, knot, id, err := repoKnotAndId(r)
256
if err != nil {
257
log.Println("failed to get repo and knot", err)
258
return
···
260
261
ref := chi.URLParam(r, "ref")
262
filePath := chi.URLParam(r, "*")
263
-
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/blob/%s/%s", knot, id.DID.String(), repoName, ref, filePath))
264
if err != nil {
265
log.Println("failed to reach knotserver", err)
266
return
···
279
return
280
}
281
282
s.pages.RepoBlob(w, pages.RepoBlobParams{
283
-
LoggedInUser: s.auth.GetUser(r),
284
RepoInfo: pages.RepoInfo{
285
-
OwnerDid: id.DID.String(),
286
-
OwnerHandle: id.Handle.String(),
287
-
Name: repoName,
288
},
289
RepoBlobResponse: result,
290
})
291
return
292
}
293
294
-
func repoKnotAndId(r *http.Request) (string, string, identity.Identity, error) {
295
repoName := chi.URLParam(r, "repo")
296
knot, ok := r.Context().Value("knot").(string)
297
if !ok {
298
log.Println("malformed middleware")
299
-
return "", "", identity.Identity{}, fmt.Errorf("malformed middleware")
300
}
301
id, ok := r.Context().Value("resolvedId").(identity.Identity)
302
if !ok {
303
log.Println("malformed middleware")
304
-
return "", "", identity.Identity{}, fmt.Errorf("malformed middleware")
305
}
306
307
-
return repoName, knot, id, nil
308
}
···
6
"io"
7
"log"
8
"net/http"
9
+
"path/filepath"
10
11
"github.com/bluesky-social/indigo/atproto/identity"
12
"github.com/go-chi/chi/v5"
13
+
"github.com/sotangled/tangled/appview/auth"
14
"github.com/sotangled/tangled/appview/pages"
15
"github.com/sotangled/tangled/types"
16
)
17
18
func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) {
19
+
f, err := fullyResolvedRepo(r)
20
if err != nil {
21
+
log.Println("failed to fully resolve repo", err)
22
return
23
}
24
25
+
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s", f.Knot, f.OwnerDid(), f.RepoName))
26
if err != nil {
27
log.Println("failed to reach knotserver", err)
28
return
···
44
45
log.Println(resp.Status, result)
46
47
+
user := s.auth.GetUser(r)
48
s.pages.RepoIndexPage(w, pages.RepoIndexParams{
49
+
LoggedInUser: user,
50
RepoInfo: pages.RepoInfo{
51
+
OwnerDid: f.OwnerDid(),
52
+
OwnerHandle: f.OwnerHandle(),
53
+
Name: f.RepoName,
54
+
SettingsAllowed: settingsAllowed(s, user, f),
55
},
56
RepoIndexResponse: result,
57
})
···
60
}
61
62
func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) {
63
+
f, err := fullyResolvedRepo(r)
64
if err != nil {
65
+
log.Println("failed to fully resolve repo", err)
66
return
67
}
68
69
ref := chi.URLParam(r, "ref")
70
+
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/log/%s", f.Knot, f.OwnerDid(), f.RepoName, ref))
71
if err != nil {
72
log.Println("failed to reach knotserver", err)
73
return
···
86
return
87
}
88
89
+
user := s.auth.GetUser(r)
90
s.pages.RepoLog(w, pages.RepoLogParams{
91
+
LoggedInUser: user,
92
RepoInfo: pages.RepoInfo{
93
+
OwnerDid: f.OwnerDid(),
94
+
OwnerHandle: f.OwnerHandle(),
95
+
Name: f.RepoName,
96
+
SettingsAllowed: settingsAllowed(s, user, f),
97
},
98
RepoLogResponse: result,
99
})
···
101
}
102
103
func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) {
104
+
f, err := fullyResolvedRepo(r)
105
if err != nil {
106
+
log.Println("failed to fully resolve repo", err)
107
return
108
}
109
110
ref := chi.URLParam(r, "ref")
111
+
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/commit/%s", f.Knot, f.OwnerDid(), f.RepoName, ref))
112
if err != nil {
113
log.Println("failed to reach knotserver", err)
114
return
···
127
return
128
}
129
130
+
user := s.auth.GetUser(r)
131
s.pages.RepoCommit(w, pages.RepoCommitParams{
132
+
LoggedInUser: user,
133
RepoInfo: pages.RepoInfo{
134
+
OwnerDid: f.OwnerDid(),
135
+
OwnerHandle: f.OwnerHandle(),
136
+
Name: f.RepoName,
137
+
SettingsAllowed: settingsAllowed(s, user, f),
138
},
139
RepoCommitResponse: result,
140
})
···
142
}
143
144
func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) {
145
+
f, err := fullyResolvedRepo(r)
146
if err != nil {
147
+
log.Println("failed to fully resolve repo", err)
148
return
149
}
150
151
ref := chi.URLParam(r, "ref")
152
treePath := chi.URLParam(r, "*")
153
+
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/tree/%s/%s", f.Knot, f.OwnerDid(), f.RepoName, ref, treePath))
154
if err != nil {
155
log.Println("failed to reach knotserver", err)
156
return
···
171
172
log.Println(result)
173
174
+
user := s.auth.GetUser(r)
175
s.pages.RepoTree(w, pages.RepoTreeParams{
176
+
LoggedInUser: user,
177
RepoInfo: pages.RepoInfo{
178
+
OwnerDid: f.OwnerDid(),
179
+
OwnerHandle: f.OwnerHandle(),
180
+
Name: f.RepoName,
181
+
SettingsAllowed: settingsAllowed(s, user, f),
182
},
183
RepoTreeResponse: result,
184
})
···
186
}
187
188
func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) {
189
+
f, err := fullyResolvedRepo(r)
190
if err != nil {
191
log.Println("failed to get repo and knot", err)
192
return
193
}
194
195
+
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/tags", f.Knot, f.OwnerDid(), f.RepoName))
196
if err != nil {
197
log.Println("failed to reach knotserver", err)
198
return
···
211
return
212
}
213
214
+
user := s.auth.GetUser(r)
215
s.pages.RepoTags(w, pages.RepoTagsParams{
216
+
LoggedInUser: user,
217
RepoInfo: pages.RepoInfo{
218
+
OwnerDid: f.OwnerDid(),
219
+
OwnerHandle: f.OwnerHandle(),
220
+
Name: f.RepoName,
221
+
SettingsAllowed: settingsAllowed(s, user, f),
222
},
223
RepoTagsResponse: result,
224
})
···
226
}
227
228
func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) {
229
+
f, err := fullyResolvedRepo(r)
230
if err != nil {
231
log.Println("failed to get repo and knot", err)
232
return
233
}
234
235
+
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/branches", f.Knot, f.OwnerDid(), f.RepoName))
236
if err != nil {
237
log.Println("failed to reach knotserver", err)
238
return
···
251
return
252
}
253
254
+
user := s.auth.GetUser(r)
255
s.pages.RepoBranches(w, pages.RepoBranchesParams{
256
+
LoggedInUser: user,
257
RepoInfo: pages.RepoInfo{
258
+
OwnerDid: f.OwnerDid(),
259
+
OwnerHandle: f.OwnerHandle(),
260
+
Name: f.RepoName,
261
+
SettingsAllowed: settingsAllowed(s, user, f),
262
},
263
RepoBranchesResponse: result,
264
})
···
266
}
267
268
func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) {
269
+
f, err := fullyResolvedRepo(r)
270
if err != nil {
271
log.Println("failed to get repo and knot", err)
272
return
···
274
275
ref := chi.URLParam(r, "ref")
276
filePath := chi.URLParam(r, "*")
277
+
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/blob/%s/%s", f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
278
if err != nil {
279
log.Println("failed to reach knotserver", err)
280
return
···
293
return
294
}
295
296
+
user := s.auth.GetUser(r)
297
s.pages.RepoBlob(w, pages.RepoBlobParams{
298
+
LoggedInUser: user,
299
RepoInfo: pages.RepoInfo{
300
+
OwnerDid: f.OwnerDid(),
301
+
OwnerHandle: f.OwnerHandle(),
302
+
Name: f.RepoName,
303
+
SettingsAllowed: settingsAllowed(s, user, f),
304
},
305
RepoBlobResponse: result,
306
})
307
return
308
}
309
310
+
func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) {
311
+
f, err := fullyResolvedRepo(r)
312
+
if err != nil {
313
+
log.Println("failed to get repo and knot", err)
314
+
return
315
+
}
316
+
317
+
collaborator := r.FormValue("collaborator")
318
+
if collaborator == "" {
319
+
http.Error(w, "malformed form", http.StatusBadRequest)
320
+
return
321
+
}
322
+
323
+
collaboratorIdent, err := s.resolver.ResolveIdent(r.Context(), collaborator)
324
+
if err != nil {
325
+
w.Write([]byte("failed to resolve collaborator did to a handle"))
326
+
return
327
+
}
328
+
log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot)
329
+
330
+
// TODO: create an atproto record for this
331
+
332
+
secret, err := s.db.GetRegistrationKey(f.Knot)
333
+
if err != nil {
334
+
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
335
+
return
336
+
}
337
+
338
+
ksClient, err := NewSignedClient(f.Knot, secret)
339
+
if err != nil {
340
+
log.Println("failed to create client to ", f.Knot)
341
+
return
342
+
}
343
+
344
+
ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())
345
+
if err != nil {
346
+
log.Printf("failed to make request to %s: %s", f.Knot, err)
347
+
return
348
+
}
349
+
350
+
if ksResp.StatusCode != http.StatusNoContent {
351
+
w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err)))
352
+
return
353
+
}
354
+
355
+
err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.OwnerSlashRepo())
356
+
if err != nil {
357
+
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
358
+
return
359
+
}
360
+
361
+
w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String())))
362
+
363
+
}
364
+
365
+
func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) {
366
+
f, err := fullyResolvedRepo(r)
367
+
if err != nil {
368
+
log.Println("failed to get repo and knot", err)
369
+
return
370
+
}
371
+
372
+
switch r.Method {
373
+
case http.MethodGet:
374
+
// for now, this is just pubkeys
375
+
user := s.auth.GetUser(r)
376
+
repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.OwnerSlashRepo(), f.Knot)
377
+
if err != nil {
378
+
log.Println("failed to get collaborators", err)
379
+
}
380
+
log.Println(repoCollaborators)
381
+
382
+
s.pages.RepoSettings(w, pages.RepoSettingsParams{
383
+
LoggedInUser: user,
384
+
Collaborators: repoCollaborators,
385
+
})
386
+
}
387
+
}
388
+
389
+
type FullyResolvedRepo struct {
390
+
Knot string
391
+
OwnerId identity.Identity
392
+
RepoName string
393
+
}
394
+
395
+
func (f *FullyResolvedRepo) OwnerDid() string {
396
+
return f.OwnerId.DID.String()
397
+
}
398
+
399
+
func (f *FullyResolvedRepo) OwnerHandle() string {
400
+
return f.OwnerId.Handle.String()
401
+
}
402
+
403
+
func (f *FullyResolvedRepo) OwnerSlashRepo() string {
404
+
return filepath.Join(f.OwnerDid(), f.RepoName)
405
+
}
406
+
407
+
func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {
408
repoName := chi.URLParam(r, "repo")
409
knot, ok := r.Context().Value("knot").(string)
410
if !ok {
411
log.Println("malformed middleware")
412
+
return nil, fmt.Errorf("malformed middleware")
413
}
414
id, ok := r.Context().Value("resolvedId").(identity.Identity)
415
if !ok {
416
log.Println("malformed middleware")
417
+
return nil, fmt.Errorf("malformed middleware")
418
}
419
420
+
return &FullyResolvedRepo{
421
+
Knot: knot,
422
+
OwnerId: id,
423
+
RepoName: repoName,
424
+
}, nil
425
+
}
426
+
427
+
func settingsAllowed(s *State, u *auth.User, f *FullyResolvedRepo) bool {
428
+
settingsAllowed := false
429
+
if u != nil {
430
+
ok, err := s.enforcer.IsSettingsAllowed(u.Did, f.Knot, f.OwnerSlashRepo())
431
+
if err == nil && ok {
432
+
settingsAllowed = true
433
+
}
434
+
}
435
+
436
+
return settingsAllowed
437
}
+18
appview/state/signer.go
+18
appview/state/signer.go
···
113
114
return s.client.Do(req)
115
}
116
+
117
+
func (s *SignedClient) AddCollaborator(ownerDid, repoName, memberDid string) (*http.Response, error) {
118
+
const (
119
+
Method = "POST"
120
+
)
121
+
endpoint := fmt.Sprintf("/{ownerDid}/{repoName}/collaborator/add")
122
+
123
+
body, _ := json.Marshal(map[string]interface{}{
124
+
"did": memberDid,
125
+
})
126
+
127
+
req, err := s.newRequest(Method, endpoint, body)
128
+
if err != nil {
129
+
return nil, err
130
+
}
131
+
132
+
return s.client.Do(req)
133
+
}
+8
-5
appview/state/state.go
+8
-5
appview/state/state.go
···
382
AddedAt: &addedAt,
383
}},
384
})
385
// invalid record
386
if err != nil {
387
log.Printf("failed to create record: %s", err)
···
563
return
564
}
565
566
-
// invalid record
567
-
if err != nil {
568
-
log.Printf("failed to create record: %s", err)
569
-
return
570
-
}
571
log.Println("created atproto record: ", resp.Uri)
572
573
return
···
611
r.Get("/info/refs", s.InfoRefs)
612
r.Post("/git-upload-pack", s.UploadPack)
613
614
})
615
})
616
···
382
AddedAt: &addedAt,
383
}},
384
})
385
+
386
// invalid record
387
if err != nil {
388
log.Printf("failed to create record: %s", err)
···
564
return
565
}
566
567
log.Println("created atproto record: ", resp.Uri)
568
569
return
···
607
r.Get("/info/refs", s.InfoRefs)
608
r.Post("/git-upload-pack", s.UploadPack)
609
610
+
// settings routes, needs auth
611
+
r.Group(func(r chi.Router) {
612
+
r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) {
613
+
r.Get("/", s.RepoSettings)
614
+
r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
615
+
})
616
+
})
617
})
618
})
619
+2
-2
knotserver/routes.go
+2
-2
knotserver/routes.go
···
515
h.jc.AddDid(data.Did)
516
517
repoName := filepath.Join(ownerDid, repo)
518
-
if err := h.e.AddRepo(data.Did, ThisServer, repoName); err != nil {
519
l.Error("adding repo collaborator", "error", err.Error())
520
writeError(w, err.Error(), http.StatusInternalServerError)
521
return
···
527
return
528
}
529
530
-
w.WriteHeader(http.StatusOK)
531
}
532
533
func (h *Handle) Init(w http.ResponseWriter, r *http.Request) {
···
515
h.jc.AddDid(data.Did)
516
517
repoName := filepath.Join(ownerDid, repo)
518
+
if err := h.e.AddCollaborator(data.Did, ThisServer, repoName); err != nil {
519
l.Error("adding repo collaborator", "error", err.Error())
520
writeError(w, err.Error(), http.StatusInternalServerError)
521
return
···
527
return
528
}
529
530
+
w.WriteHeader(http.StatusNoContent)
531
}
532
533
func (h *Handle) Init(w http.ResponseWriter, r *http.Request) {
+24
rbac/rbac.go
+24
rbac/rbac.go
···
2
3
import (
4
"database/sql"
5
"path"
6
"strings"
7
···
95
}
96
97
func (e *Enforcer) AddRepo(member, domain, repo string) error {
98
_, err := e.E.AddPolicies([][]string{
99
{member, domain, repo, "repo:push"},
100
{member, domain, repo, "repo:owner"},
101
{member, domain, repo, "repo:invite"},
102
{member, domain, repo, "repo:delete"},
103
{"server:owner", domain, repo, "repo:delete"}, // server owner can delete any repo
104
})
105
return err
106
}
···
137
138
func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) {
139
return e.E.Enforce(user, domain, repo, "repo:push")
140
}
141
142
// keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin
···
2
3
import (
4
"database/sql"
5
+
"fmt"
6
"path"
7
"strings"
8
···
96
}
97
98
func (e *Enforcer) AddRepo(member, domain, repo string) error {
99
+
// sanity check, repo must be of the form ownerDid/repo
100
+
if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") {
101
+
return fmt.Errorf("invalid repo: %s", repo)
102
+
}
103
+
104
_, err := e.E.AddPolicies([][]string{
105
+
{member, domain, repo, "repo:settings"},
106
{member, domain, repo, "repo:push"},
107
{member, domain, repo, "repo:owner"},
108
{member, domain, repo, "repo:invite"},
109
{member, domain, repo, "repo:delete"},
110
{"server:owner", domain, repo, "repo:delete"}, // server owner can delete any repo
111
+
})
112
+
return err
113
+
}
114
+
115
+
func (e *Enforcer) AddCollaborator(collaborator, domain, repo string) error {
116
+
// sanity check, repo must be of the form ownerDid/repo
117
+
if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") {
118
+
return fmt.Errorf("invalid repo: %s", repo)
119
+
}
120
+
121
+
_, err := e.E.AddPolicies([][]string{
122
+
{collaborator, domain, repo, "repo:settings"},
123
+
{collaborator, domain, repo, "repo:push"},
124
})
125
return err
126
}
···
157
158
func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) {
159
return e.E.Enforce(user, domain, repo, "repo:push")
160
+
}
161
+
162
+
func (e *Enforcer) IsSettingsAllowed(user, domain, repo string) (bool, error) {
163
+
return e.E.Enforce(user, domain, repo, "repo:settings")
164
}
165
166
// keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin