-5
appview/db/profile.go
-5
appview/db/profile.go
···
2
2
3
3
import (
4
4
"fmt"
5
-
"log"
6
5
"sort"
7
6
"time"
8
7
)
···
65
64
return timeline, fmt.Errorf("error getting all repos by did: %w", err)
66
65
}
67
66
68
-
log.Println(repos)
69
-
70
67
for _, repo := range repos {
71
68
var sourceRepo *Repo
72
-
log.Println("name", repo.Name)
73
69
if repo.Source != "" {
74
-
log.Println("source", repo.Source)
75
70
sourceRepo, err = GetRepoByAtUri(e, repo.Source)
76
71
if err != nil {
77
72
return nil, err
+2
-2
appview/db/repos.go
+2
-2
appview/db/repos.go
···
176
176
return err
177
177
}
178
178
179
-
func RemoveRepo(e Execer, did, name, rkey string) error {
180
-
_, err := e.Exec(`delete from repos where did = ? and name = ? and rkey = ?`, did, name, rkey)
179
+
func RemoveRepo(e Execer, did, name string) error {
180
+
_, err := e.Exec(`delete from repos where did = ? and name = ?`, did, name)
181
181
return err
182
182
}
183
183
+8
appview/pages/pages.go
+8
appview/pages/pages.go
···
246
246
return slices.Contains(r.Roles, "repo:settings")
247
247
}
248
248
249
+
func (r RolesInRepo) CollaboratorInviteAllowed() bool {
250
+
return slices.Contains(r.Roles, "repo:invite")
251
+
}
252
+
253
+
func (r RolesInRepo) RepoDeleteAllowed() bool {
254
+
return slices.Contains(r.Roles, "repo:delete")
255
+
}
256
+
249
257
func (r RolesInRepo) IsOwner() bool {
250
258
return slices.Contains(r.Roles, "repo:owner")
251
259
}
+14
-3
appview/pages/templates/repo/settings.html
+14
-3
appview/pages/templates/repo/settings.html
···
22
22
{{ end }}
23
23
</div>
24
24
25
-
{{ if .IsCollaboratorInviteAllowed }}
25
+
{{ if .RepoInfo.Roles.CollaboratorInviteAllowed }}
26
26
<form hx-put="/{{ $.RepoInfo.FullName }}/settings/collaborator">
27
27
<label for="collaborator" class="dark:text-white"
28
28
>add collaborator</label
···
45
45
{{ end }}
46
46
47
47
<form hx-put="/{{ $.RepoInfo.FullName }}/settings/branches/default" class="mt-6">
48
-
<label for="branch">default branch:</label>
49
-
<select id="branch" name="branch" required class="p-1 border border-gray-200 bg-white">
48
+
<label for="branch">default branch</label>
49
+
<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">
50
50
{{ range .Branches }}
51
51
<option
52
52
value="{{ . }}"
···
61
61
</select>
62
62
<button class="btn my-2" type="text">save</button>
63
63
</form>
64
+
65
+
{{ if .RepoInfo.Roles.RepoDeleteAllowed }}
66
+
<form hx-confirm="Are you sure you want to delete this repository?" hx-delete="/{{ $.RepoInfo.FullName }}/settings/delete" class="mt-6">
67
+
<label for="branch">delete repository</label>
68
+
<button class="btn my-2" type="text">delete</button>
69
+
<span>
70
+
Deleting a repository is irreversible and permanent.
71
+
</span>
72
+
</form>
73
+
{{ end }}
74
+
64
75
{{ end }}
+106
appview/state/repo.go
+106
appview/state/repo.go
···
596
596
597
597
}
598
598
599
+
func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) {
600
+
user := s.auth.GetUser(r)
601
+
602
+
f, err := fullyResolvedRepo(r)
603
+
if err != nil {
604
+
log.Println("failed to get repo and knot", err)
605
+
return
606
+
}
607
+
608
+
// remove record from pds
609
+
xrpcClient, _ := s.auth.AuthorizedClient(r)
610
+
repoRkey := f.RepoAt.RecordKey().String()
611
+
_, err = comatproto.RepoDeleteRecord(r.Context(), xrpcClient, &comatproto.RepoDeleteRecord_Input{
612
+
Collection: tangled.RepoNSID,
613
+
Repo: user.Did,
614
+
Rkey: repoRkey,
615
+
})
616
+
if err != nil {
617
+
log.Printf("failed to delete record: %s", err)
618
+
s.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.")
619
+
return
620
+
}
621
+
log.Println("removed repo record ", f.RepoAt.String())
622
+
623
+
secret, err := db.GetRegistrationKey(s.db, f.Knot)
624
+
if err != nil {
625
+
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
626
+
return
627
+
}
628
+
629
+
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
630
+
if err != nil {
631
+
log.Println("failed to create client to ", f.Knot)
632
+
return
633
+
}
634
+
635
+
ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName)
636
+
if err != nil {
637
+
log.Printf("failed to make request to %s: %s", f.Knot, err)
638
+
return
639
+
}
640
+
641
+
if ksResp.StatusCode != http.StatusNoContent {
642
+
log.Println("failed to remove repo from knot, continuing anyway ", f.Knot)
643
+
} else {
644
+
log.Println("removed repo from knot ", f.Knot)
645
+
}
646
+
647
+
tx, err := s.db.BeginTx(r.Context(), nil)
648
+
if err != nil {
649
+
log.Println("failed to start tx")
650
+
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
651
+
return
652
+
}
653
+
defer func() {
654
+
tx.Rollback()
655
+
err = s.enforcer.E.LoadPolicy()
656
+
if err != nil {
657
+
log.Println("failed to rollback policies")
658
+
}
659
+
}()
660
+
661
+
// remove collaborator RBAC
662
+
repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.OwnerSlashRepo(), f.Knot)
663
+
if err != nil {
664
+
s.pages.Notice(w, "settings-delete", "Failed to remove collaborators")
665
+
return
666
+
}
667
+
for _, c := range repoCollaborators {
668
+
did := c[0]
669
+
s.enforcer.RemoveCollaborator(did, f.Knot, f.OwnerSlashRepo())
670
+
}
671
+
log.Println("removed collaborators")
672
+
673
+
// remove repo RBAC
674
+
err = s.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.OwnerSlashRepo())
675
+
if err != nil {
676
+
s.pages.Notice(w, "settings-delete", "Failed to update RBAC rules")
677
+
return
678
+
}
679
+
680
+
// remove repo from db
681
+
err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName)
682
+
if err != nil {
683
+
s.pages.Notice(w, "settings-delete", "Failed to update appview")
684
+
return
685
+
}
686
+
log.Println("removed repo from db")
687
+
688
+
err = tx.Commit()
689
+
if err != nil {
690
+
log.Println("failed to commit changes", err)
691
+
http.Error(w, err.Error(), http.StatusInternalServerError)
692
+
return
693
+
}
694
+
695
+
err = s.enforcer.E.SavePolicy()
696
+
if err != nil {
697
+
log.Println("failed to update ACLs", err)
698
+
http.Error(w, err.Error(), http.StatusInternalServerError)
699
+
return
700
+
}
701
+
702
+
s.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid()))
703
+
}
704
+
599
705
func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
600
706
f, err := fullyResolvedRepo(r)
601
707
if err != nil {
+1
appview/state/router.go
+1
appview/state/router.go
···
153
153
r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) {
154
154
r.Get("/", s.RepoSettings)
155
155
r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
156
+
r.With(RepoPermissionMiddleware(s, "repo:delete")).Delete("/delete", s.DeleteRepo)
156
157
r.Put("/branches/default", s.SetDefaultBranch)
157
158
})
158
159
})
+57
-20
rbac/rbac.go
+57
-20
rbac/rbac.go
···
96
96
return err
97
97
}
98
98
99
-
func (e *Enforcer) AddRepo(member, domain, repo string) error {
100
-
// sanity check, repo must be of the form ownerDid/repo
101
-
if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") {
102
-
return fmt.Errorf("invalid repo: %s", repo)
103
-
}
104
-
105
-
_, err := e.E.AddPolicies([][]string{
99
+
func repoPolicies(member, domain, repo string) [][]string {
100
+
return [][]string{
106
101
{member, domain, repo, "repo:settings"},
107
102
{member, domain, repo, "repo:push"},
108
103
{member, domain, repo, "repo:owner"},
109
104
{member, domain, repo, "repo:invite"},
110
105
{member, domain, repo, "repo:delete"},
111
106
{"server:owner", domain, repo, "repo:delete"}, // server owner can delete any repo
112
-
})
107
+
}
108
+
}
109
+
func (e *Enforcer) AddRepo(member, domain, repo string) error {
110
+
err := checkRepoFormat(repo)
111
+
if err != nil {
112
+
return err
113
+
}
114
+
115
+
_, err = e.E.AddPolicies(repoPolicies(member, domain, repo))
116
+
return err
117
+
}
118
+
func (e *Enforcer) RemoveRepo(member, domain, repo string) error {
119
+
err := checkRepoFormat(repo)
120
+
if err != nil {
121
+
return err
122
+
}
123
+
124
+
_, err = e.E.RemovePolicies(repoPolicies(member, domain, repo))
113
125
return err
114
126
}
115
127
128
+
var (
129
+
collaboratorPolicies = func(collaborator, domain, repo string) [][]string {
130
+
return [][]string{
131
+
{collaborator, domain, repo, "repo:collaborator"},
132
+
{collaborator, domain, repo, "repo:settings"},
133
+
{collaborator, domain, repo, "repo:push"},
134
+
}
135
+
}
136
+
)
137
+
116
138
func (e *Enforcer) AddCollaborator(collaborator, domain, repo string) error {
117
-
// sanity check, repo must be of the form ownerDid/repo
118
-
if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") {
119
-
return fmt.Errorf("invalid repo: %s", repo)
139
+
err := checkRepoFormat(repo)
140
+
if err != nil {
141
+
return err
120
142
}
121
143
122
-
_, err := e.E.AddPolicies([][]string{
123
-
{collaborator, domain, repo, "repo:collaborator"},
124
-
{collaborator, domain, repo, "repo:settings"},
125
-
{collaborator, domain, repo, "repo:push"},
126
-
})
144
+
_, err = e.E.AddPolicies(collaboratorPolicies(collaborator, domain, repo))
145
+
return err
146
+
}
147
+
148
+
func (e *Enforcer) RemoveCollaborator(collaborator, domain, repo string) error {
149
+
err := checkRepoFormat(repo)
150
+
if err != nil {
151
+
return err
152
+
}
153
+
154
+
_, err = e.E.RemovePolicies(collaboratorPolicies(collaborator, domain, repo))
127
155
return err
128
156
}
129
157
···
165
193
return e.E.Enforce(user, domain, repo, "repo:settings")
166
194
}
167
195
196
+
func (e *Enforcer) IsCollaboratorInviteAllowed(user, domain, repo string) (bool, error) {
197
+
return e.E.Enforce(user, domain, repo, "repo:invite")
198
+
}
199
+
168
200
// given a repo, what permissions does this user have? repo:owner? repo:invite? etc.
169
201
func (e *Enforcer) GetPermissionsInRepo(user, domain, repo string) []string {
170
202
var permissions []string
···
179
211
return permissions
180
212
}
181
213
182
-
func (e *Enforcer) IsCollaboratorInviteAllowed(user, domain, repo string) (bool, error) {
183
-
return e.E.Enforce(user, domain, repo, "repo:invite")
184
-
}
185
-
186
214
// keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin
187
215
func keyMatch2Func(args ...interface{}) (interface{}, error) {
188
216
name1 := args[0].(string)
···
190
218
191
219
return keyMatch2(name1, name2), nil
192
220
}
221
+
222
+
func checkRepoFormat(repo string) error {
223
+
// sanity check, repo must be of the form ownerDid/repo
224
+
if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") {
225
+
return fmt.Errorf("invalid repo: %s", repo)
226
+
}
227
+
228
+
return nil
229
+
}