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

feat: repo deletion

this capability has existed in knots since the first release

Changed files
+188 -30
appview
db
pages
templates
state
rbac
-5
appview/db/profile.go
···
import (
"fmt"
-
"log"
"sort"
"time"
)
···
return timeline, fmt.Errorf("error getting all repos by did: %w", err)
}
-
log.Println(repos)
-
for _, repo := range repos {
var sourceRepo *Repo
-
log.Println("name", repo.Name)
if repo.Source != "" {
-
log.Println("source", repo.Source)
sourceRepo, err = GetRepoByAtUri(e, repo.Source)
if err != nil {
return nil, err
···
import (
"fmt"
"sort"
"time"
)
···
return timeline, fmt.Errorf("error getting all repos by did: %w", err)
}
for _, repo := range repos {
var sourceRepo *Repo
if repo.Source != "" {
sourceRepo, err = GetRepoByAtUri(e, repo.Source)
if err != nil {
return nil, err
+2 -2
appview/db/repos.go
···
return err
}
-
func RemoveRepo(e Execer, did, name, rkey string) error {
-
_, err := e.Exec(`delete from repos where did = ? and name = ? and rkey = ?`, did, name, rkey)
return err
}
···
return err
}
+
func RemoveRepo(e Execer, did, name string) error {
+
_, err := e.Exec(`delete from repos where did = ? and name = ?`, did, name)
return err
}
+8
appview/pages/pages.go
···
return slices.Contains(r.Roles, "repo:settings")
}
func (r RolesInRepo) IsOwner() bool {
return slices.Contains(r.Roles, "repo:owner")
}
···
return slices.Contains(r.Roles, "repo:settings")
}
+
func (r RolesInRepo) CollaboratorInviteAllowed() bool {
+
return slices.Contains(r.Roles, "repo:invite")
+
}
+
+
func (r RolesInRepo) RepoDeleteAllowed() bool {
+
return slices.Contains(r.Roles, "repo:delete")
+
}
+
func (r RolesInRepo) IsOwner() bool {
return slices.Contains(r.Roles, "repo:owner")
}
+14 -3
appview/pages/templates/repo/settings.html
···
{{ end }}
</div>
-
{{ if .IsCollaboratorInviteAllowed }}
<form hx-put="/{{ $.RepoInfo.FullName }}/settings/collaborator">
<label for="collaborator" class="dark:text-white"
>add collaborator</label
···
{{ end }}
<form hx-put="/{{ $.RepoInfo.FullName }}/settings/branches/default" class="mt-6">
-
<label for="branch">default branch:</label>
-
<select id="branch" name="branch" required class="p-1 border border-gray-200 bg-white">
{{ range .Branches }}
<option
value="{{ . }}"
···
</select>
<button class="btn my-2" type="text">save</button>
</form>
{{ end }}
···
{{ end }}
</div>
+
{{ if .RepoInfo.Roles.CollaboratorInviteAllowed }}
<form hx-put="/{{ $.RepoInfo.FullName }}/settings/collaborator">
<label for="collaborator" class="dark:text-white"
>add collaborator</label
···
{{ end }}
<form hx-put="/{{ $.RepoInfo.FullName }}/settings/branches/default" class="mt-6">
+
<label for="branch">default branch</label>
+
<select id="branch" name="branch" required class="p-1 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700">
{{ range .Branches }}
<option
value="{{ . }}"
···
</select>
<button class="btn my-2" type="text">save</button>
</form>
+
+
{{ if .RepoInfo.Roles.RepoDeleteAllowed }}
+
<form hx-confirm="Are you sure you want to delete this repository?" hx-delete="/{{ $.RepoInfo.FullName }}/settings/delete" class="mt-6">
+
<label for="branch">delete repository</label>
+
<button class="btn my-2" type="text">delete</button>
+
<span>
+
Deleting a repository is irreversible and permanent.
+
</span>
+
</form>
+
{{ end }}
+
{{ end }}
+106
appview/state/repo.go
···
}
func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
f, err := fullyResolvedRepo(r)
if err != nil {
···
}
+
func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) {
+
user := s.auth.GetUser(r)
+
+
f, err := fullyResolvedRepo(r)
+
if err != nil {
+
log.Println("failed to get repo and knot", err)
+
return
+
}
+
+
// remove record from pds
+
xrpcClient, _ := s.auth.AuthorizedClient(r)
+
repoRkey := f.RepoAt.RecordKey().String()
+
_, err = comatproto.RepoDeleteRecord(r.Context(), xrpcClient, &comatproto.RepoDeleteRecord_Input{
+
Collection: tangled.RepoNSID,
+
Repo: user.Did,
+
Rkey: repoRkey,
+
})
+
if err != nil {
+
log.Printf("failed to delete record: %s", err)
+
s.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.")
+
return
+
}
+
log.Println("removed repo record ", f.RepoAt.String())
+
+
secret, err := db.GetRegistrationKey(s.db, 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, s.config.Dev)
+
if err != nil {
+
log.Println("failed to create client to ", f.Knot)
+
return
+
}
+
+
ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName)
+
if err != nil {
+
log.Printf("failed to make request to %s: %s", f.Knot, err)
+
return
+
}
+
+
if ksResp.StatusCode != http.StatusNoContent {
+
log.Println("failed to remove repo from knot, continuing anyway ", f.Knot)
+
} else {
+
log.Println("removed repo from knot ", f.Knot)
+
}
+
+
tx, err := s.db.BeginTx(r.Context(), nil)
+
if err != nil {
+
log.Println("failed to start tx")
+
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
+
return
+
}
+
defer func() {
+
tx.Rollback()
+
err = s.enforcer.E.LoadPolicy()
+
if err != nil {
+
log.Println("failed to rollback policies")
+
}
+
}()
+
+
// remove collaborator RBAC
+
repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.OwnerSlashRepo(), f.Knot)
+
if err != nil {
+
s.pages.Notice(w, "settings-delete", "Failed to remove collaborators")
+
return
+
}
+
for _, c := range repoCollaborators {
+
did := c[0]
+
s.enforcer.RemoveCollaborator(did, f.Knot, f.OwnerSlashRepo())
+
}
+
log.Println("removed collaborators")
+
+
// remove repo RBAC
+
err = s.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.OwnerSlashRepo())
+
if err != nil {
+
s.pages.Notice(w, "settings-delete", "Failed to update RBAC rules")
+
return
+
}
+
+
// remove repo from db
+
err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName)
+
if err != nil {
+
s.pages.Notice(w, "settings-delete", "Failed to update appview")
+
return
+
}
+
log.Println("removed repo from db")
+
+
err = tx.Commit()
+
if err != nil {
+
log.Println("failed to commit changes", err)
+
http.Error(w, err.Error(), http.StatusInternalServerError)
+
return
+
}
+
+
err = s.enforcer.E.SavePolicy()
+
if err != nil {
+
log.Println("failed to update ACLs", err)
+
http.Error(w, err.Error(), http.StatusInternalServerError)
+
return
+
}
+
+
s.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid()))
+
}
+
func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
f, err := fullyResolvedRepo(r)
if err != nil {
+1
appview/state/router.go
···
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)
r.Put("/branches/default", s.SetDefaultBranch)
})
})
···
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)
+
r.With(RepoPermissionMiddleware(s, "repo:delete")).Delete("/delete", s.DeleteRepo)
r.Put("/branches/default", s.SetDefaultBranch)
})
})
+57 -20
rbac/rbac.go
···
return err
}
-
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:collaborator"},
-
{collaborator, domain, repo, "repo:settings"},
-
{collaborator, domain, repo, "repo:push"},
-
})
return err
}
···
return e.E.Enforce(user, domain, repo, "repo:settings")
}
// given a repo, what permissions does this user have? repo:owner? repo:invite? etc.
func (e *Enforcer) GetPermissionsInRepo(user, domain, repo string) []string {
var permissions []string
···
return permissions
}
-
func (e *Enforcer) IsCollaboratorInviteAllowed(user, domain, repo string) (bool, error) {
-
return e.E.Enforce(user, domain, repo, "repo:invite")
-
}
-
// keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin
func keyMatch2Func(args ...interface{}) (interface{}, error) {
name1 := args[0].(string)
···
return keyMatch2(name1, name2), nil
}
···
return err
}
+
func repoPolicies(member, domain, repo string) [][]string {
+
return [][]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
+
}
+
}
+
func (e *Enforcer) AddRepo(member, domain, repo string) error {
+
err := checkRepoFormat(repo)
+
if err != nil {
+
return err
+
}
+
+
_, err = e.E.AddPolicies(repoPolicies(member, domain, repo))
+
return err
+
}
+
func (e *Enforcer) RemoveRepo(member, domain, repo string) error {
+
err := checkRepoFormat(repo)
+
if err != nil {
+
return err
+
}
+
+
_, err = e.E.RemovePolicies(repoPolicies(member, domain, repo))
return err
}
+
var (
+
collaboratorPolicies = func(collaborator, domain, repo string) [][]string {
+
return [][]string{
+
{collaborator, domain, repo, "repo:collaborator"},
+
{collaborator, domain, repo, "repo:settings"},
+
{collaborator, domain, repo, "repo:push"},
+
}
+
}
+
)
+
func (e *Enforcer) AddCollaborator(collaborator, domain, repo string) error {
+
err := checkRepoFormat(repo)
+
if err != nil {
+
return err
}
+
_, err = e.E.AddPolicies(collaboratorPolicies(collaborator, domain, repo))
+
return err
+
}
+
+
func (e *Enforcer) RemoveCollaborator(collaborator, domain, repo string) error {
+
err := checkRepoFormat(repo)
+
if err != nil {
+
return err
+
}
+
+
_, err = e.E.RemovePolicies(collaboratorPolicies(collaborator, domain, repo))
return err
}
···
return e.E.Enforce(user, domain, repo, "repo:settings")
}
+
func (e *Enforcer) IsCollaboratorInviteAllowed(user, domain, repo string) (bool, error) {
+
return e.E.Enforce(user, domain, repo, "repo:invite")
+
}
+
// given a repo, what permissions does this user have? repo:owner? repo:invite? etc.
func (e *Enforcer) GetPermissionsInRepo(user, domain, repo string) []string {
var permissions []string
···
return permissions
}
// keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin
func keyMatch2Func(args ...interface{}) (interface{}, error) {
name1 := args[0].(string)
···
return keyMatch2(name1, name2), nil
}
+
+
func checkRepoFormat(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)
+
}
+
+
return nil
+
}