forked from tangled.org/core
this repo has no description

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

Changed files
+210 -3
db
routes
+80
db/access.go
···
+
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
+
}
+
+
}
+10 -1
db/init.go
···
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
+35
routes/access.go
···
+
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)
+
})
+
}
+
}
+10 -2
routes/handler.go
···
"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"
)
···
}
}
-
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)
···
r.Get("/archive/{file}", h.Archive)
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)
+75
routes/routes.go
···
}
}
+
// 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))
···
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
}