···33import (
44 "fmt"
55 "net/http"
66- "slices"
76 "strconv"
8798 "tangled.org/core/api/tangled"
···364363 }
365364366365 // user can only delete branch if they are a collaborator in the repo that the branch belongs to
367367- perms := s.enforcer.GetPermissionsInRepo(user.Did, repo.Knot, repo.RepoIdentifier())
368368- if !slices.Contains(perms, "repo:push") {
366366+ if !s.acl.HasRepoPermission(r.Context(), repo, user.Did, "repo:push") {
369367 return nil
370368 }
371369
+185-10
appview/repo/repo.go
···1515 "tangled.org/core/appview/cloudflare"
16161717 "tangled.org/core/api/tangled"
1818- "tangled.org/core/appview/compat113"
1918 "tangled.org/core/appview/config"
2019 "tangled.org/core/appview/db"
2020+ "tangled.org/core/appview/knotacl"
2121+ "tangled.org/core/appview/knotcompat"
2122 "tangled.org/core/appview/models"
2223 "tangled.org/core/appview/notify"
2324 "tangled.org/core/appview/oauth"
···2728 "tangled.org/core/appview/sites"
2829 "tangled.org/core/appview/validator"
2930 xrpcclient "tangled.org/core/appview/xrpcclient"
3131+ "tangled.org/core/consts"
3032 "tangled.org/core/eventconsumer"
3133 "tangled.org/core/idresolver"
3234 "tangled.org/core/ogre"
···5254 spindlestream *eventconsumer.Consumer
5355 db *db.DB
5456 enforcer *rbac.Enforcer
5757+ acl *knotacl.Service
5558 notifier notify.Notifier
5659 logger *slog.Logger
5760 serviceAuth *serviceauth.ServiceAuth
···7073 config *config.Config,
7174 notifier notify.Notifier,
7275 enforcer *rbac.Enforcer,
7676+ acl *knotacl.Service,
7377 logger *slog.Logger,
7478 validator *validator.Validator,
7579 cfClient *cloudflare.Client,
···8488 db: db,
8589 notifier: notifier,
8690 enforcer: enforcer,
9191+ acl: acl,
8792 logger: logger,
8893 validator: validator,
8994 cfClient: cfClient,
···754759 l = l.With("collaborator", collaboratorIdent.Handle)
755760 l = l.With("knot", f.Knot)
756761762762+ if knotcompat.KnotHasCapability(r.Context(), f.Knot, rp.config.Core.Dev, consts.CapKnotACL) {
763763+ if f.RepoDid == "" {
764764+ fail("This repository is missing its DID and cannot manage collaborators.", nil)
765765+ return
766766+ }
767767+768768+ client, err := rp.oauth.ServiceClient(
769769+ r,
770770+ oauth.WithService(f.Knot),
771771+ oauth.WithLxm(tangled.RepoAddCollaboratorNSID),
772772+ oauth.WithDev(rp.config.Core.Dev),
773773+ )
774774+ if err != nil {
775775+ fail("Failed to connect to knot server.", err)
776776+ return
777777+ }
778778+779779+ err = tangled.RepoAddCollaborator(r.Context(), client, &tangled.RepoAddCollaborator_Input{
780780+ Repo: f.RepoDid,
781781+ Subject: collaboratorIdent.DID.String(),
782782+ })
783783+ if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
784784+ l.Error("failed to call XRPC repo.addCollaborator", "xrpcerr", xrpcerr, "err", err)
785785+ rp.pages.Notice(w, errorId, xrpcerr.Error())
786786+ return
787787+ }
788788+789789+ rp.acl.InvalidateCollaborators(f.Knot, f.RepoDid)
790790+791791+ rp.pages.HxRefresh(w)
792792+ return
793793+ }
794794+757795 existing, err := db.GetCollaborators(rp.db,
758796 orm.FilterEq("repo_did", f.RepoDid),
759797 orm.FilterEq("subject_did", collaboratorIdent.DID.String()),
···782820 Collection: tangled.RepoCollaboratorNSID,
783821 Repo: currentUser.Did,
784822 Rkey: rkey,
785785- Record: compat113.Collaborator(repoCollaboratorRecord(f, collaboratorIdent.DID.String(), createdAt)),
823823+ Record: knotcompat.Collaborator(repoCollaboratorRecord(f, collaboratorIdent.DID.String(), createdAt)),
786824 })
787825 // invalid record
788826 if err != nil {
···853891 rp.pages.HxRefresh(w)
854892}
855893894894+func (rp *Repo) RemoveCollaborator(w http.ResponseWriter, r *http.Request) {
895895+ user := rp.oauth.GetMultiAccountUser(r)
896896+ l := rp.logger.With("handler", "RemoveCollaborator")
897897+ l = l.With("did", user.Did)
898898+899899+ f, err := rp.repoResolver.Resolve(r)
900900+ if err != nil {
901901+ l.Error("failed to get repo and knot", "err", err)
902902+ return
903903+ }
904904+905905+ errorId := "collaborator-error"
906906+ fail := func(msg string, err error) {
907907+ l.Error(msg, "err", err)
908908+ rp.pages.Notice(w, errorId, msg)
909909+ }
910910+911911+ collaborator := r.FormValue("collaborator")
912912+ if collaborator == "" {
913913+ fail("Invalid form.", nil)
914914+ return
915915+ }
916916+ collaborator = strings.TrimPrefix(collaborator, "@")
917917+918918+ collaboratorIdent, err := rp.idResolver.ResolveIdent(r.Context(), collaborator)
919919+ if err != nil {
920920+ fail(fmt.Sprintf("'%s' is not a valid DID/handle.", collaborator), err)
921921+ return
922922+ }
923923+ l = l.With("collaborator", collaboratorIdent.Handle, "knot", f.Knot)
924924+925925+ if collaboratorIdent.DID.String() == f.Did {
926926+ fail("Cannot remove the repository owner.", nil)
927927+ return
928928+ }
929929+930930+ if knotcompat.KnotHasCapability(r.Context(), f.Knot, rp.config.Core.Dev, consts.CapKnotACL) {
931931+ if f.RepoDid == "" {
932932+ fail("This repository is missing its DID and cannot manage collaborators.", nil)
933933+ return
934934+ }
935935+936936+ client, err := rp.oauth.ServiceClient(
937937+ r,
938938+ oauth.WithService(f.Knot),
939939+ oauth.WithLxm(tangled.RepoRemoveCollaboratorNSID),
940940+ oauth.WithDev(rp.config.Core.Dev),
941941+ )
942942+ if err != nil {
943943+ fail("Failed to connect to knot server.", err)
944944+ return
945945+ }
946946+947947+ err = tangled.RepoRemoveCollaborator(r.Context(), client, &tangled.RepoRemoveCollaborator_Input{
948948+ Repo: f.RepoDid,
949949+ Subject: collaboratorIdent.DID.String(),
950950+ })
951951+ if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
952952+ l.Error("failed to call XRPC repo.removeCollaborator", "xrpcerr", xrpcerr, "err", err)
953953+ rp.pages.Notice(w, errorId, xrpcerr.Error())
954954+ return
955955+ }
956956+957957+ rp.acl.InvalidateCollaborators(f.Knot, f.RepoDid)
958958+959959+ rp.pages.HxRefresh(w)
960960+ return
961961+ }
962962+963963+ existing, err := db.GetCollaborators(rp.db,
964964+ orm.FilterEq("repo_did", f.RepoDid),
965965+ orm.FilterEq("subject_did", collaboratorIdent.DID.String()),
966966+ )
967967+ if err != nil {
968968+ fail("Failed to look up collaborator.", err)
969969+ return
970970+ }
971971+ if len(existing) == 0 {
972972+ fail(fmt.Sprintf("%s is not a collaborator.", collaboratorIdent.Handle), nil)
973973+ return
974974+ }
975975+ row := existing[0]
976976+977977+ client, err := rp.oauth.AuthorizedClient(r)
978978+ if err != nil {
979979+ fail("Failed to write to PDS.", err)
980980+ return
981981+ }
982982+983983+ tx, err := rp.db.BeginTx(r.Context(), nil)
984984+ if err != nil {
985985+ fail("Failed to remove collaborator.", err)
986986+ return
987987+ }
988988+ committed := false
989989+ defer func() {
990990+ if !committed {
991991+ tx.Rollback()
992992+ if err := rp.enforcer.E.LoadPolicy(); err != nil {
993993+ l.Error("failed to reload policy after rollback", "err", err)
994994+ }
995995+ }
996996+ }()
997997+998998+ if err := rp.enforcer.RemoveCollaborator(collaboratorIdent.DID.String(), f.Knot, f.RepoIdentifier()); err != nil {
999999+ fail("Failed to remove collaborator permissions.", err)
10001000+ return
10011001+ }
10021002+10031003+ if err := db.DeleteCollaborator(tx,
10041004+ orm.FilterEq("repo_did", f.RepoDid),
10051005+ orm.FilterEq("subject_did", collaboratorIdent.DID.String()),
10061006+ ); err != nil {
10071007+ fail("Failed to remove collaborator.", err)
10081008+ return
10091009+ }
10101010+10111011+ if row.Rkey.Valid && row.Rkey.String != "" {
10121012+ if _, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
10131013+ Collection: tangled.RepoCollaboratorNSID,
10141014+ Repo: row.Did.String(),
10151015+ Rkey: row.Rkey.String,
10161016+ }); err != nil {
10171017+ fail("Failed to delete collaborator record from PDS.", err)
10181018+ return
10191019+ }
10201020+ }
10211021+10221022+ if err := tx.Commit(); err != nil {
10231023+ fail("Failed to remove collaborator.", err)
10241024+ return
10251025+ }
10261026+ committed = true
10271027+10281028+ if err := rp.enforcer.E.SavePolicy(); err != nil {
10291029+ fail("Failed to update collaborator permissions.", err)
10301030+ return
10311031+ }
10321032+10331033+ rp.pages.HxRefresh(w)
10341034+}
10351035+8561036func (rp *Repo) RenameRepo(w http.ResponseWriter, r *http.Request) {
8571037 l := rp.logger.With("handler", "RenameRepo")
8581038 noticeId := "rename-repo-error"
···8711051 return
8721052 }
8731053874874- if !compat113.KnotSupports114(r.Context(), f.Knot, rp.config.Core.Dev) {
10541054+ if !knotcompat.KnotSupports114(r.Context(), f.Knot, rp.config.Core.Dev) {
8751055 rp.pages.Notice(w, noticeId, "This repository's knot is below v1.14 and does not yet support renames. Ask the knot operator to upgrade.")
8761056 return
8771057 }
···12421422 switch r.Method {
12431423 case http.MethodGet:
12441424 user := rp.oauth.GetMultiAccountUser(r)
12451245- knots, err := rp.enforcer.GetKnotsForUser(user.Did)
12461246- if err != nil {
12471247- rp.pages.Notice(w, "repo", "Invalid user account.")
12481248- return
12491249- }
14251425+ knots := rp.acl.KnotsForUser(r.Context(), user.Did)
1250142612511427 rp.pages.ForkRepo(w, pages.ForkRepoParams{
12521428 LoggedInUser: user,
···12641440 }
12651441 l = l.With("targetKnot", targetKnot)
1266144212671267- ok, err := rp.enforcer.E.Enforce(user.Did, targetKnot, targetKnot, "repo:create")
12681268- if err != nil || !ok {
14431443+ if !rp.acl.IsRepoCreateAllowed(r.Context(), targetKnot, user.Did) {
12691444 rp.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
12701445 return
12711446 }