Monorepo for Tangled tangled.org

add not-working repo settings

Changed files
+309 -86
appview
knotserver
rbac
+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
··· 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>&nbsp;· 18 - <a href="/{{ .RepoInfo.FullName }}/branches">branches</a>&nbsp;· 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>&nbsp;· 19 + <a href="/{{ .RepoInfo.FullName }}/branches">branches</a>&nbsp;· 20 + <a href="/{{ .RepoInfo.FullName }}/tags">tags</a> 21 + {{ if .RepoInfo.SettingsAllowed }} 22 + ·&nbsp;<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
··· 1 - {{ define "repoContent" }} 2 {{ $name := .RepoInfo.Name }} 3 <h3>branches</h3> 4 <div class="refs">
··· 1 + {{ define "title" }} 2 + branches | {{ .RepoInfo.FullName }} 3 + {{ end }} 4 + 5 + {{ define "content" }} 6 {{ $name := .RepoInfo.Name }} 7 <h3>branches</h3> 8 <div class="refs">
+6
appview/pages/templates/repo/settings.html
···
··· 1 + {{define "repoContent"}} 2 + <main> 3 + <h1>settings</h1> 4 + </main> 5 + {{end}} 6 +
+30
appview/state/middleware.go
··· 104 } 105 } 106 107 func StripLeadingAt(next http.Handler) http.Handler { 108 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 109 path := req.URL.Path
··· 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
··· 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
··· 113 114 return s.client.Do(req) 115 }
··· 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
··· 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
··· 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
··· 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