forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

add not-working repo settings

Changed files
+309 -86
appview
knotserver
rbac
+14 -5
appview/pages/pages.go
···
}
type RepoInfo struct {
-
Name string
-
OwnerDid string
-
OwnerHandle string
-
Description string
+
Name string
+
OwnerDid string
+
OwnerHandle string
+
Description string
+
SettingsAllowed bool
}
func (r RepoInfo) OwnerWithAt() string {
···
}
func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error {
-
return p.executeRepo("repo/index", w, params)
}
···
func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {
return p.executeRepo("repo/blob", w, params)
+
}
+
+
type RepoSettingsParams struct {
+
LoggedInUser *auth.User
+
Collaborators [][]string
+
}
+
+
func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error {
+
return p.executeRepo("repo/settings", w, params)
}
func (p *Pages) Static() http.Handler {
+23 -23
appview/pages/templates/layouts/repobase.html
···
{{ define "content" }}
-
<div id="repo-header">
-
<h1>{{ .RepoInfo.FullName }}</h1>
-
{{ if .RepoInfo.Description }}
-
<h3 class="desc">{{ .RepoInfo.Description }}</h3>
-
{{ else }}
-
<em>this repo has no description</em>
-
</div>
-
-
{{ with .IsEmpty }}
-
{{ else }}
-
<div id="repo-links">
-
<nav>
-
<a href="/{{ .RepoInfo.FullName }}">summary</a>&nbsp;·
-
<a href="/{{ .RepoInfo.FullName }}/branches">branches</a>&nbsp;·
-
<a href="/{{ .RepoInfo.FullName }}/tags">tags</a>
-
</nav>
-
<div>
-
{{ end }}
+
<div id="repo-header">
+
<h1>{{ .RepoInfo.FullName }}</h1>
+
{{ if .RepoInfo.Description }}
+
<h3 class="desc">{{ .RepoInfo.Description }}</h3>
+
{{ else }}
+
<em>this repo has no description</em>
+
{{ end }}
+
</div>
-
{{ end }}
+
{{ with .IsEmpty }}
+
{{ else }}
+
<div id="repo-links">
+
<nav>
+
<a href="/{{ .RepoInfo.FullName }}">summary</a>&nbsp;·
+
<a href="/{{ .RepoInfo.FullName }}/branches">branches</a>&nbsp;·
+
<a href="/{{ .RepoInfo.FullName }}/tags">tags</a>
+
{{ if .RepoInfo.SettingsAllowed }}
+
·&nbsp;<a href="/{{ .RepoInfo.FullName }}/settings">settings</a>
+
{{ end }}
+
</nav>
+
<div>
+
{{ end }}
-
{{ block "repoContent" . }} {{ end }}
+
{{ block "repoContent" . }} {{ end }}
{{ end }}
{{ define "layouts/repobase" }}
-
-
{{ template "layouts/base" . }}
-
+
{{ template "layouts/base" . }}
{{ end }}
+5 -1
appview/pages/templates/repo/branches.html
···
-
{{ define "repoContent" }}
+
{{ define "title" }}
+
branches | {{ .RepoInfo.FullName }}
+
{{ end }}
+
+
{{ define "content" }}
{{ $name := .RepoInfo.Name }}
<h3>branches</h3>
<div class="refs">
+6
appview/pages/templates/repo/settings.html
···
+
{{define "repoContent"}}
+
<main>
+
<h1>settings</h1>
+
</main>
+
{{end}}
+
+30
appview/state/middleware.go
···
}
}
+
func RepoPermissionMiddleware(s *State, requiredPerm string) Middleware {
+
return func(next http.Handler) http.Handler {
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
// requires auth also
+
actor := s.auth.GetUser(r)
+
if actor == nil {
+
// we need a logged in user
+
log.Printf("not logged in, redirecting")
+
http.Error(w, "Forbiden", http.StatusUnauthorized)
+
return
+
}
+
f, err := fullyResolvedRepo(r)
+
if err != nil {
+
http.Error(w, "malformed url", http.StatusBadRequest)
+
return
+
}
+
+
ok, err := s.enforcer.E.Enforce(actor.Did, f.Knot, f.OwnerSlashRepo(), requiredPerm)
+
if err != nil || !ok {
+
// we need a logged in user
+
log.Printf("%s does not have perms of a %s in repo %s", actor.Did, requiredPerm, f.OwnerSlashRepo())
+
http.Error(w, "Forbiden", http.StatusUnauthorized)
+
return
+
}
+
+
next.ServeHTTP(w, r)
+
})
+
}
+
}
+
func StripLeadingAt(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path
+179 -50
appview/state/repo.go
···
"io"
"log"
"net/http"
+
"path/filepath"
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/go-chi/chi/v5"
+
"github.com/sotangled/tangled/appview/auth"
"github.com/sotangled/tangled/appview/pages"
"github.com/sotangled/tangled/types"
)
func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) {
-
repoName, knot, id, err := repoKnotAndId(r)
+
f, err := fullyResolvedRepo(r)
if err != nil {
-
log.Println("failed to get repo and knot", err)
+
log.Println("failed to fully resolve repo", err)
return
}
-
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s", knot, id.DID.String(), repoName))
+
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s", f.Knot, f.OwnerDid(), f.RepoName))
if err != nil {
log.Println("failed to reach knotserver", err)
return
···
log.Println(resp.Status, result)
+
user := s.auth.GetUser(r)
s.pages.RepoIndexPage(w, pages.RepoIndexParams{
-
LoggedInUser: s.auth.GetUser(r),
+
LoggedInUser: user,
RepoInfo: pages.RepoInfo{
-
OwnerDid: id.DID.String(),
-
OwnerHandle: id.Handle.String(),
-
Name: repoName,
+
OwnerDid: f.OwnerDid(),
+
OwnerHandle: f.OwnerHandle(),
+
Name: f.RepoName,
+
SettingsAllowed: settingsAllowed(s, user, f),
},
RepoIndexResponse: result,
})
···
}
func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) {
-
repoName, knot, id, err := repoKnotAndId(r)
+
f, err := fullyResolvedRepo(r)
if err != nil {
-
log.Println("failed to get repo and knot", err)
+
log.Println("failed to fully resolve repo", err)
return
}
ref := chi.URLParam(r, "ref")
-
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/log/%s", knot, id.DID.String(), repoName, ref))
+
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/log/%s", f.Knot, f.OwnerDid(), f.RepoName, ref))
if err != nil {
log.Println("failed to reach knotserver", err)
return
···
return
}
+
user := s.auth.GetUser(r)
s.pages.RepoLog(w, pages.RepoLogParams{
-
LoggedInUser: s.auth.GetUser(r),
+
LoggedInUser: user,
RepoInfo: pages.RepoInfo{
-
OwnerDid: id.DID.String(),
-
OwnerHandle: id.Handle.String(),
-
Name: repoName,
+
OwnerDid: f.OwnerDid(),
+
OwnerHandle: f.OwnerHandle(),
+
Name: f.RepoName,
+
SettingsAllowed: settingsAllowed(s, user, f),
},
RepoLogResponse: result,
})
···
}
func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) {
-
repoName, knot, id, err := repoKnotAndId(r)
+
f, err := fullyResolvedRepo(r)
if err != nil {
-
log.Println("failed to get repo and knot", err)
+
log.Println("failed to fully resolve repo", err)
return
}
ref := chi.URLParam(r, "ref")
-
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/commit/%s", knot, id.DID.String(), repoName, ref))
+
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/commit/%s", f.Knot, f.OwnerDid(), f.RepoName, ref))
if err != nil {
log.Println("failed to reach knotserver", err)
return
···
return
}
+
user := s.auth.GetUser(r)
s.pages.RepoCommit(w, pages.RepoCommitParams{
-
LoggedInUser: s.auth.GetUser(r),
+
LoggedInUser: user,
RepoInfo: pages.RepoInfo{
-
OwnerDid: id.DID.String(),
-
OwnerHandle: id.Handle.String(),
-
Name: repoName,
+
OwnerDid: f.OwnerDid(),
+
OwnerHandle: f.OwnerHandle(),
+
Name: f.RepoName,
+
SettingsAllowed: settingsAllowed(s, user, f),
},
RepoCommitResponse: result,
})
···
}
func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) {
-
repoName, knot, id, err := repoKnotAndId(r)
+
f, err := fullyResolvedRepo(r)
if err != nil {
-
log.Println("failed to get repo and knot", err)
+
log.Println("failed to fully resolve repo", err)
return
}
ref := chi.URLParam(r, "ref")
treePath := chi.URLParam(r, "*")
-
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/tree/%s/%s", knot, id.DID.String(), repoName, ref, treePath))
+
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/tree/%s/%s", f.Knot, f.OwnerDid(), f.RepoName, ref, treePath))
if err != nil {
log.Println("failed to reach knotserver", err)
return
···
log.Println(result)
+
user := s.auth.GetUser(r)
s.pages.RepoTree(w, pages.RepoTreeParams{
-
LoggedInUser: s.auth.GetUser(r),
+
LoggedInUser: user,
RepoInfo: pages.RepoInfo{
-
OwnerDid: id.DID.String(),
-
OwnerHandle: id.Handle.String(),
-
Name: repoName,
+
OwnerDid: f.OwnerDid(),
+
OwnerHandle: f.OwnerHandle(),
+
Name: f.RepoName,
+
SettingsAllowed: settingsAllowed(s, user, f),
},
RepoTreeResponse: result,
})
···
}
func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) {
-
repoName, knot, id, err := repoKnotAndId(r)
+
f, err := fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
-
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/tags", knot, id.DID.String(), repoName))
+
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/tags", f.Knot, f.OwnerDid(), f.RepoName))
if err != nil {
log.Println("failed to reach knotserver", err)
return
···
return
}
+
user := s.auth.GetUser(r)
s.pages.RepoTags(w, pages.RepoTagsParams{
-
LoggedInUser: s.auth.GetUser(r),
+
LoggedInUser: user,
RepoInfo: pages.RepoInfo{
-
OwnerDid: id.DID.String(),
-
OwnerHandle: id.Handle.String(),
-
Name: repoName,
+
OwnerDid: f.OwnerDid(),
+
OwnerHandle: f.OwnerHandle(),
+
Name: f.RepoName,
+
SettingsAllowed: settingsAllowed(s, user, f),
},
RepoTagsResponse: result,
})
···
}
func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) {
-
repoName, knot, id, err := repoKnotAndId(r)
+
f, err := fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
-
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/branches", knot, id.DID.String(), repoName))
+
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/branches", f.Knot, f.OwnerDid(), f.RepoName))
if err != nil {
log.Println("failed to reach knotserver", err)
return
···
return
}
+
user := s.auth.GetUser(r)
s.pages.RepoBranches(w, pages.RepoBranchesParams{
-
LoggedInUser: s.auth.GetUser(r),
+
LoggedInUser: user,
RepoInfo: pages.RepoInfo{
-
OwnerDid: id.DID.String(),
-
OwnerHandle: id.Handle.String(),
-
Name: repoName,
+
OwnerDid: f.OwnerDid(),
+
OwnerHandle: f.OwnerHandle(),
+
Name: f.RepoName,
+
SettingsAllowed: settingsAllowed(s, user, f),
},
RepoBranchesResponse: result,
})
···
}
func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) {
-
repoName, knot, id, err := repoKnotAndId(r)
+
f, err := fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
ref := chi.URLParam(r, "ref")
filePath := chi.URLParam(r, "*")
-
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/blob/%s/%s", knot, id.DID.String(), repoName, ref, filePath))
+
resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/blob/%s/%s", f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
if err != nil {
log.Println("failed to reach knotserver", err)
return
···
return
}
+
user := s.auth.GetUser(r)
s.pages.RepoBlob(w, pages.RepoBlobParams{
-
LoggedInUser: s.auth.GetUser(r),
+
LoggedInUser: user,
RepoInfo: pages.RepoInfo{
-
OwnerDid: id.DID.String(),
-
OwnerHandle: id.Handle.String(),
-
Name: repoName,
+
OwnerDid: f.OwnerDid(),
+
OwnerHandle: f.OwnerHandle(),
+
Name: f.RepoName,
+
SettingsAllowed: settingsAllowed(s, user, f),
},
RepoBlobResponse: result,
})
return
}
-
func repoKnotAndId(r *http.Request) (string, string, identity.Identity, error) {
+
func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) {
+
f, err := fullyResolvedRepo(r)
+
if err != nil {
+
log.Println("failed to get repo and knot", err)
+
return
+
}
+
+
collaborator := r.FormValue("collaborator")
+
if collaborator == "" {
+
http.Error(w, "malformed form", http.StatusBadRequest)
+
return
+
}
+
+
collaboratorIdent, err := s.resolver.ResolveIdent(r.Context(), collaborator)
+
if err != nil {
+
w.Write([]byte("failed to resolve collaborator did to a handle"))
+
return
+
}
+
log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot)
+
+
// TODO: create an atproto record for this
+
+
secret, err := s.db.GetRegistrationKey(f.Knot)
+
if err != nil {
+
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
+
return
+
}
+
+
ksClient, err := NewSignedClient(f.Knot, secret)
+
if err != nil {
+
log.Println("failed to create client to ", f.Knot)
+
return
+
}
+
+
ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())
+
if err != nil {
+
log.Printf("failed to make request to %s: %s", f.Knot, err)
+
return
+
}
+
+
if ksResp.StatusCode != http.StatusNoContent {
+
w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err)))
+
return
+
}
+
+
err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.OwnerSlashRepo())
+
if err != nil {
+
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
+
return
+
}
+
+
w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String())))
+
+
}
+
+
func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) {
+
f, err := fullyResolvedRepo(r)
+
if err != nil {
+
log.Println("failed to get repo and knot", err)
+
return
+
}
+
+
switch r.Method {
+
case http.MethodGet:
+
// for now, this is just pubkeys
+
user := s.auth.GetUser(r)
+
repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.OwnerSlashRepo(), f.Knot)
+
if err != nil {
+
log.Println("failed to get collaborators", err)
+
}
+
log.Println(repoCollaborators)
+
+
s.pages.RepoSettings(w, pages.RepoSettingsParams{
+
LoggedInUser: user,
+
Collaborators: repoCollaborators,
+
})
+
}
+
}
+
+
type FullyResolvedRepo struct {
+
Knot string
+
OwnerId identity.Identity
+
RepoName string
+
}
+
+
func (f *FullyResolvedRepo) OwnerDid() string {
+
return f.OwnerId.DID.String()
+
}
+
+
func (f *FullyResolvedRepo) OwnerHandle() string {
+
return f.OwnerId.Handle.String()
+
}
+
+
func (f *FullyResolvedRepo) OwnerSlashRepo() string {
+
return filepath.Join(f.OwnerDid(), f.RepoName)
+
}
+
+
func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {
repoName := chi.URLParam(r, "repo")
knot, ok := r.Context().Value("knot").(string)
if !ok {
log.Println("malformed middleware")
-
return "", "", identity.Identity{}, fmt.Errorf("malformed middleware")
+
return nil, fmt.Errorf("malformed middleware")
}
id, ok := r.Context().Value("resolvedId").(identity.Identity)
if !ok {
log.Println("malformed middleware")
-
return "", "", identity.Identity{}, fmt.Errorf("malformed middleware")
+
return nil, fmt.Errorf("malformed middleware")
}
-
return repoName, knot, id, nil
+
return &FullyResolvedRepo{
+
Knot: knot,
+
OwnerId: id,
+
RepoName: repoName,
+
}, nil
+
}
+
+
func settingsAllowed(s *State, u *auth.User, f *FullyResolvedRepo) bool {
+
settingsAllowed := false
+
if u != nil {
+
ok, err := s.enforcer.IsSettingsAllowed(u.Did, f.Knot, f.OwnerSlashRepo())
+
if err == nil && ok {
+
settingsAllowed = true
+
}
+
}
+
+
return settingsAllowed
}
+18
appview/state/signer.go
···
return s.client.Do(req)
}
+
+
func (s *SignedClient) AddCollaborator(ownerDid, repoName, memberDid string) (*http.Response, error) {
+
const (
+
Method = "POST"
+
)
+
endpoint := fmt.Sprintf("/{ownerDid}/{repoName}/collaborator/add")
+
+
body, _ := json.Marshal(map[string]interface{}{
+
"did": memberDid,
+
})
+
+
req, err := s.newRequest(Method, endpoint, body)
+
if err != nil {
+
return nil, err
+
}
+
+
return s.client.Do(req)
+
}
+8 -5
appview/state/state.go
···
AddedAt: &addedAt,
}},
})
+
// invalid record
if err != nil {
log.Printf("failed to create record: %s", err)
···
return
}
-
// invalid record
-
if err != nil {
-
log.Printf("failed to create record: %s", err)
-
return
-
}
log.Println("created atproto record: ", resp.Uri)
return
···
r.Get("/info/refs", s.InfoRefs)
r.Post("/git-upload-pack", s.UploadPack)
+
// settings routes, needs auth
+
r.Group(func(r chi.Router) {
+
r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) {
+
r.Get("/", s.RepoSettings)
+
r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
+
})
+
})
})
})
+2 -2
knotserver/routes.go
···
h.jc.AddDid(data.Did)
repoName := filepath.Join(ownerDid, repo)
-
if err := h.e.AddRepo(data.Did, ThisServer, repoName); err != nil {
+
if err := h.e.AddCollaborator(data.Did, ThisServer, repoName); err != nil {
l.Error("adding repo collaborator", "error", err.Error())
writeError(w, err.Error(), http.StatusInternalServerError)
return
···
return
}
-
w.WriteHeader(http.StatusOK)
+
w.WriteHeader(http.StatusNoContent)
}
func (h *Handle) Init(w http.ResponseWriter, r *http.Request) {
+24
rbac/rbac.go
···
import (
"database/sql"
+
"fmt"
"path"
"strings"
···
}
func (e *Enforcer) AddRepo(member, domain, repo string) error {
+
// sanity check, repo must be of the form ownerDid/repo
+
if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") {
+
return fmt.Errorf("invalid repo: %s", repo)
+
}
+
_, err := e.E.AddPolicies([][]string{
+
{member, domain, repo, "repo:settings"},
{member, domain, repo, "repo:push"},
{member, domain, repo, "repo:owner"},
{member, domain, repo, "repo:invite"},
{member, domain, repo, "repo:delete"},
{"server:owner", domain, repo, "repo:delete"}, // server owner can delete any repo
+
})
+
return err
+
}
+
+
func (e *Enforcer) AddCollaborator(collaborator, domain, repo string) error {
+
// sanity check, repo must be of the form ownerDid/repo
+
if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") {
+
return fmt.Errorf("invalid repo: %s", repo)
+
}
+
+
_, err := e.E.AddPolicies([][]string{
+
{collaborator, domain, repo, "repo:settings"},
+
{collaborator, domain, repo, "repo:push"},
})
return err
}
···
func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) {
return e.E.Enforce(user, domain, repo, "repo:push")
+
}
+
+
func (e *Enforcer) IsSettingsAllowed(user, domain, repo string) (bool, error) {
+
return e.E.Enforce(user, domain, repo, "repo:settings")
}
// keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin