From b527eba4a36b5b4135f9b866d773019faffaf4e5 Mon Sep 17 00:00:00 2001 From: Akshay Date: Tue, 28 Jan 2025 20:11:59 +0000 Subject: [PATCH] implement access levels - new table tracks access levels between each DID and a repo - creators of a repo are owners by default - newly added members are writers by default - introduces AccessLevel middleware to mask routes based on level --- db/access.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++ db/init.go | 11 ++++++- routes/access.go | 35 +++++++++++++++++++++ routes/handler.go | 12 +++++-- routes/routes.go | 75 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 db/access.go create mode 100644 routes/access.go diff --git a/db/access.go b/db/access.go new file mode 100644 index 0000000..7a802da --- /dev/null +++ b/db/access.go @@ -0,0 +1,80 @@ +package db + +import ( + "log" + "strings" +) + +// forms a poset +type Level int + +const ( + Reader Level = iota + Writer + Owner +) + +var ( + levelMap = map[string]Level{ + "writer": Writer, + "owner": Owner, + } +) + +func ParseLevel(str string) (Level, bool) { + c, ok := levelMap[strings.ToLower(str)] + return c, ok +} + +func (l Level) String() string { + switch l { + case Owner: + return "OWNER" + case Writer: + return "WRITER" + case Reader: + return "READER" + default: + return "READER" + } +} + +func (d *DB) SetAccessLevel(userDid string, repoDid string, repoName string, level Level) error { + _, err := d.db.Exec( + `insert + into access_levels (repo_id, did, access) + values ((select id from repos where did = $1 and name = $2), $3, $4) + on conflict (repo_id, did) + do update set access = $4;`, + repoDid, repoName, userDid, level.String()) + return err +} + +func (d *DB) SetOwner(userDid string, repoDid string, repoName string) error { + return d.SetAccessLevel(userDid, repoDid, repoName, Owner) +} + +func (d *DB) SetWriter(userDid string, repoDid string, repoName string) error { + return d.SetAccessLevel(userDid, repoDid, repoName, Writer) +} + +func (d *DB) GetAccessLevel(userDid string, repoDid string, repoName string) (Level, error) { + row := d.db.QueryRow(` + select access_levels.access + from repos + join access_levels + on repos.id = access_levels.repo_id + where access_levels.did = ? and repos.did = ? and repos.name = ? + `, userDid, repoDid, repoName) + + var levelStr string + err := row.Scan(&levelStr) + if err != nil { + log.Println(err) + return Reader, err + } else { + level, _ := ParseLevel(levelStr) + return level, nil + } + +} diff --git a/db/init.go b/db/init.go index ce6958a..0e20c83 100644 --- a/db/init.go +++ b/db/init.go @@ -32,7 +32,16 @@ func Setup(dbPath string) (*DB, error) { description text not null, created timestamp default current_timestamp, unique(did, name) - ) + ); + create table if not exists access_levels ( + id integer primary key autoincrement, + repo_id integer not null, + did text not null, + access text not null check (access in ('OWNER', 'WRITER')), + created timestamp default current_timestamp, + unique(repo_id, did), + foreign key (repo_id) references repos(id) on delete cascade + ); `) if err != nil { return nil, err diff --git a/routes/access.go b/routes/access.go new file mode 100644 index 0000000..fa75a05 --- /dev/null +++ b/routes/access.go @@ -0,0 +1,35 @@ +package routes + +import ( + "github.com/go-chi/chi/v5" + "github.com/icyphox/bild/db" + auth "github.com/icyphox/bild/routes/auth" + "log" + "net/http" +) + +func (h *Handle) AccessLevel(level db.Level) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + repoOwnerHandle := chi.URLParam(r, "user") + repoOwner, err := auth.ResolveIdent(r.Context(), repoOwnerHandle) + if err != nil { + log.Println("invalid did") + http.Error(w, "invalid did", http.StatusNotFound) + return + } + repoName := chi.URLParam(r, "name") + session, _ := h.s.Get(r, "bild-session") + did := session.Values["did"].(string) + + userLevel, err := h.db.GetAccessLevel(did, repoOwner.DID.String(), repoName) + if err != nil || userLevel < level { + log.Printf("unauthorized access: %s accessing %s/%s\n", did, repoOwnerHandle, repoName) + log.Printf("wanted level: %s, got level %s", level.String(), userLevel.String()) + http.Error(w, "Forbidden", http.StatusUnauthorized) + return + } + next.ServeHTTP(w, r) + }) + } +} diff --git a/routes/handler.go b/routes/handler.go index f1134d1..7e6d13f 100644 --- a/routes/handler.go +++ b/routes/handler.go @@ -11,7 +11,7 @@ import ( "github.com/gorilla/sessions" "github.com/icyphox/bild/auth" "github.com/icyphox/bild/config" - "github.com/icyphox/bild/db" + database "github.com/icyphox/bild/db" "github.com/icyphox/bild/routes/middleware" "github.com/icyphox/bild/routes/tmpl" ) @@ -38,7 +38,7 @@ func (h *Handle) Multiplex(w http.ResponseWriter, r *http.Request) { } } -func Setup(c *config.Config, db *db.DB) (http.Handler, error) { +func Setup(c *config.Config, db *database.DB) (http.Handler, error) { r := chi.NewRouter() s := sessions.NewCookieStore([]byte("TODO_CHANGE_ME")) t, err := tmpl.Load(c.Dirs.Templates) @@ -100,6 +100,14 @@ func Setup(c *config.Config, db *db.DB) (http.Handler, error) { r.Get("/commit/{ref}", h.Diff) r.Get("/refs/", h.Refs) + r.Group(func(r chi.Router) { + // settings page is only accessible to owners + r.Use(h.AccessLevel(database.Owner)) + r.Route("/settings", func(r chi.Router) { + r.Put("/collaborators", h.Collaborators) + }) + }) + // Catch-all routes r.Get("/*", h.Multiplex) r.Post("/*", h.Multiplex) diff --git a/routes/routes.go b/routes/routes.go index 62437d8..753ceee 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -431,6 +431,64 @@ func (h *Handle) Refs(w http.ResponseWriter, r *http.Request) { } } +// func (h *Handle) addUserToRepo(w http.ResponseWriter, r *http.Request) { +// repoOwnerHandle := chi.URLParam(r, "user") +// repoOwner, err := auth.ResolveIdent(r.Context(), repoOwnerHandle) +// if err != nil { +// log.Println("invalid did") +// http.Error(w, "invalid did", http.StatusNotFound) +// return +// } +// repoName := chi.URLParam(r, "name") +// session, _ := h.s.Get(r, "bild-session") +// did := session.Values["did"].(string) +// +// err := h.db.SetWriter() +// } +func (h *Handle) Collaborators(w http.ResponseWriter, r *http.Request) { + // put repo resolution in middleware + repoOwnerHandle := chi.URLParam(r, "user") + repoOwner, err := auth.ResolveIdent(r.Context(), repoOwnerHandle) + if err != nil { + log.Println("invalid did") + http.Error(w, "invalid did", http.StatusNotFound) + return + } + repoName := chi.URLParam(r, "name") + + switch r.Method { + case http.MethodGet: + // TODO fetch a list of collaborators and their access rights + http.Error(w, "unimpl 1", http.StatusInternalServerError) + return + case http.MethodPut: + newUser := r.FormValue("newUser") + if newUser == "" { + // TODO: htmx this + http.Error(w, "unimpl 2", http.StatusInternalServerError) + return + } + newUserIdentity, err := auth.ResolveIdent(r.Context(), newUser) + if err != nil { + // TODO: htmx this + log.Println("invalid handle") + http.Error(w, "unimpl 3", http.StatusBadRequest) + return + } + err = h.db.SetWriter(newUserIdentity.DID.String(), repoOwner.DID.String(), repoName) + if err != nil { + // TODO: htmx this + log.Println("failed to add collaborator") + http.Error(w, "unimpl 4", http.StatusInternalServerError) + return + } + + log.Println("success") + return + + } +} + func (h *Handle) ServeStatic(w http.ResponseWriter, r *http.Request) { f := chi.URLParam(r, "file") f = filepath.Clean(filepath.Join(h.c.Dirs.Static, f)) @@ -554,8 +612,25 @@ func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) { return } + // For use by repoguard + didPath := filepath.Join(repoPath, "did") + err = os.WriteFile(didPath, []byte(did), 0644) + if err != nil { + h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.") + return + } + + // TODO: add repo & setting-to-owner must happen in the same transaction err = h.db.AddRepo(did, name, description) if err != nil { + log.Println(err) + h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.") + return + } + // current user is set to owner of did/name repo + err = h.db.SetOwner(did, did, name) + if err != nil { + log.Println(err) h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.") return } -- 2.51.2