+80
db/access.go
+80
db/access.go
···
1
+
package db
2
+
3
+
import (
4
+
"log"
5
+
"strings"
6
+
)
7
+
8
+
// forms a poset
9
+
type Level int
10
+
11
+
const (
12
+
Reader Level = iota
13
+
Writer
14
+
Owner
15
+
)
16
+
17
+
var (
18
+
levelMap = map[string]Level{
19
+
"writer": Writer,
20
+
"owner": Owner,
21
+
}
22
+
)
23
+
24
+
func ParseLevel(str string) (Level, bool) {
25
+
c, ok := levelMap[strings.ToLower(str)]
26
+
return c, ok
27
+
}
28
+
29
+
func (l Level) String() string {
30
+
switch l {
31
+
case Owner:
32
+
return "OWNER"
33
+
case Writer:
34
+
return "WRITER"
35
+
case Reader:
36
+
return "READER"
37
+
default:
38
+
return "READER"
39
+
}
40
+
}
41
+
42
+
func (d *DB) SetAccessLevel(userDid string, repoDid string, repoName string, level Level) error {
43
+
_, err := d.db.Exec(
44
+
`insert
45
+
into access_levels (repo_id, did, access)
46
+
values ((select id from repos where did = $1 and name = $2), $3, $4)
47
+
on conflict (repo_id, did)
48
+
do update set access = $4;`,
49
+
repoDid, repoName, userDid, level.String())
50
+
return err
51
+
}
52
+
53
+
func (d *DB) SetOwner(userDid string, repoDid string, repoName string) error {
54
+
return d.SetAccessLevel(userDid, repoDid, repoName, Owner)
55
+
}
56
+
57
+
func (d *DB) SetWriter(userDid string, repoDid string, repoName string) error {
58
+
return d.SetAccessLevel(userDid, repoDid, repoName, Writer)
59
+
}
60
+
61
+
func (d *DB) GetAccessLevel(userDid string, repoDid string, repoName string) (Level, error) {
62
+
row := d.db.QueryRow(`
63
+
select access_levels.access
64
+
from repos
65
+
join access_levels
66
+
on repos.id = access_levels.repo_id
67
+
where access_levels.did = ? and repos.did = ? and repos.name = ?
68
+
`, userDid, repoDid, repoName)
69
+
70
+
var levelStr string
71
+
err := row.Scan(&levelStr)
72
+
if err != nil {
73
+
log.Println(err)
74
+
return Reader, err
75
+
} else {
76
+
level, _ := ParseLevel(levelStr)
77
+
return level, nil
78
+
}
79
+
80
+
}
+10
-1
db/init.go
+10
-1
db/init.go
···
32
32
description text not null,
33
33
created timestamp default current_timestamp,
34
34
unique(did, name)
35
-
)
35
+
);
36
+
create table if not exists access_levels (
37
+
id integer primary key autoincrement,
38
+
repo_id integer not null,
39
+
did text not null,
40
+
access text not null check (access in ('OWNER', 'WRITER')),
41
+
created timestamp default current_timestamp,
42
+
unique(repo_id, did),
43
+
foreign key (repo_id) references repos(id) on delete cascade
44
+
);
36
45
`)
37
46
if err != nil {
38
47
return nil, err
+35
routes/access.go
+35
routes/access.go
···
1
+
package routes
2
+
3
+
import (
4
+
"github.com/go-chi/chi/v5"
5
+
"github.com/icyphox/bild/db"
6
+
auth "github.com/icyphox/bild/routes/auth"
7
+
"log"
8
+
"net/http"
9
+
)
10
+
11
+
func (h *Handle) AccessLevel(level db.Level) func(http.Handler) http.Handler {
12
+
return func(next http.Handler) http.Handler {
13
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
14
+
repoOwnerHandle := chi.URLParam(r, "user")
15
+
repoOwner, err := auth.ResolveIdent(r.Context(), repoOwnerHandle)
16
+
if err != nil {
17
+
log.Println("invalid did")
18
+
http.Error(w, "invalid did", http.StatusNotFound)
19
+
return
20
+
}
21
+
repoName := chi.URLParam(r, "name")
22
+
session, _ := h.s.Get(r, "bild-session")
23
+
did := session.Values["did"].(string)
24
+
25
+
userLevel, err := h.db.GetAccessLevel(did, repoOwner.DID.String(), repoName)
26
+
if err != nil || userLevel < level {
27
+
log.Printf("unauthorized access: %s accessing %s/%s\n", did, repoOwnerHandle, repoName)
28
+
log.Printf("wanted level: %s, got level %s", level.String(), userLevel.String())
29
+
http.Error(w, "Forbidden", http.StatusUnauthorized)
30
+
return
31
+
}
32
+
next.ServeHTTP(w, r)
33
+
})
34
+
}
35
+
}
+10
-2
routes/handler.go
+10
-2
routes/handler.go
···
11
11
"github.com/gorilla/sessions"
12
12
"github.com/icyphox/bild/auth"
13
13
"github.com/icyphox/bild/config"
14
-
"github.com/icyphox/bild/db"
14
+
database "github.com/icyphox/bild/db"
15
15
"github.com/icyphox/bild/routes/middleware"
16
16
"github.com/icyphox/bild/routes/tmpl"
17
17
)
···
38
38
}
39
39
}
40
40
41
-
func Setup(c *config.Config, db *db.DB) (http.Handler, error) {
41
+
func Setup(c *config.Config, db *database.DB) (http.Handler, error) {
42
42
r := chi.NewRouter()
43
43
s := sessions.NewCookieStore([]byte("TODO_CHANGE_ME"))
44
44
t, err := tmpl.Load(c.Dirs.Templates)
···
99
99
r.Get("/archive/{file}", h.Archive)
100
100
r.Get("/commit/{ref}", h.Diff)
101
101
r.Get("/refs/", h.Refs)
102
+
103
+
r.Group(func(r chi.Router) {
104
+
// settings page is only accessible to owners
105
+
r.Use(h.AccessLevel(database.Owner))
106
+
r.Route("/settings", func(r chi.Router) {
107
+
r.Put("/collaborators", h.Collaborators)
108
+
})
109
+
})
102
110
103
111
// Catch-all routes
104
112
r.Get("/*", h.Multiplex)
+75
routes/routes.go
+75
routes/routes.go
···
431
431
}
432
432
}
433
433
434
+
// func (h *Handle) addUserToRepo(w http.ResponseWriter, r *http.Request) {
435
+
// repoOwnerHandle := chi.URLParam(r, "user")
436
+
// repoOwner, err := auth.ResolveIdent(r.Context(), repoOwnerHandle)
437
+
// if err != nil {
438
+
// log.Println("invalid did")
439
+
// http.Error(w, "invalid did", http.StatusNotFound)
440
+
// return
441
+
// }
442
+
// repoName := chi.URLParam(r, "name")
443
+
// session, _ := h.s.Get(r, "bild-session")
444
+
// did := session.Values["did"].(string)
445
+
//
446
+
// err := h.db.SetWriter()
447
+
// }
448
+
func (h *Handle) Collaborators(w http.ResponseWriter, r *http.Request) {
449
+
// put repo resolution in middleware
450
+
repoOwnerHandle := chi.URLParam(r, "user")
451
+
repoOwner, err := auth.ResolveIdent(r.Context(), repoOwnerHandle)
452
+
if err != nil {
453
+
log.Println("invalid did")
454
+
http.Error(w, "invalid did", http.StatusNotFound)
455
+
return
456
+
}
457
+
repoName := chi.URLParam(r, "name")
458
+
459
+
switch r.Method {
460
+
case http.MethodGet:
461
+
// TODO fetch a list of collaborators and their access rights
462
+
http.Error(w, "unimpl 1", http.StatusInternalServerError)
463
+
return
464
+
case http.MethodPut:
465
+
newUser := r.FormValue("newUser")
466
+
if newUser == "" {
467
+
// TODO: htmx this
468
+
http.Error(w, "unimpl 2", http.StatusInternalServerError)
469
+
return
470
+
}
471
+
newUserIdentity, err := auth.ResolveIdent(r.Context(), newUser)
472
+
if err != nil {
473
+
// TODO: htmx this
474
+
log.Println("invalid handle")
475
+
http.Error(w, "unimpl 3", http.StatusBadRequest)
476
+
return
477
+
}
478
+
err = h.db.SetWriter(newUserIdentity.DID.String(), repoOwner.DID.String(), repoName)
479
+
if err != nil {
480
+
// TODO: htmx this
481
+
log.Println("failed to add collaborator")
482
+
http.Error(w, "unimpl 4", http.StatusInternalServerError)
483
+
return
484
+
}
485
+
486
+
log.Println("success")
487
+
return
488
+
489
+
}
490
+
}
491
+
434
492
func (h *Handle) ServeStatic(w http.ResponseWriter, r *http.Request) {
435
493
f := chi.URLParam(r, "file")
436
494
f = filepath.Clean(filepath.Join(h.c.Dirs.Static, f))
···
554
612
return
555
613
}
556
614
615
+
// For use by repoguard
616
+
didPath := filepath.Join(repoPath, "did")
617
+
err = os.WriteFile(didPath, []byte(did), 0644)
618
+
if err != nil {
619
+
h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
620
+
return
621
+
}
622
+
623
+
// TODO: add repo & setting-to-owner must happen in the same transaction
557
624
err = h.db.AddRepo(did, name, description)
558
625
if err != nil {
626
+
log.Println(err)
627
+
h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
628
+
return
629
+
}
630
+
// current user is set to owner of did/name repo
631
+
err = h.db.SetOwner(did, did, name)
632
+
if err != nil {
633
+
log.Println(err)
559
634
h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
560
635
return
561
636
}