Monorepo for Tangled tangled.org

feat(repo): add collaborator removal #1123

open opened by murex.tngl.sh targeting master from murex.tngl.sh/tangled: feat/remove-collaborator
  • Add RemoveCollaborator handler: delete PDS record, RBAC policy, and DB row
  • Add DELETE /settings/collaborator route (repo:owner)
  • Load collaborator rkey from DB for removal
  • Add trash icon remove button on access settings

close: https://tangled.org/tangled.org/core/issues/210

Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:owyua2lvxbs55wyhs22dqu2s/sh.tangled.repo.pull/3mgjrpxldea22
+151 -1
Interdiff #0 โ†’ #1
appview/pages/pages.go

This file has not been changed.

appview/pages/templates/repo/settings/access.html

This file has not been changed.

appview/repo/repo.go

This file has not been changed.

appview/repo/router.go

This file has not been changed.

appview/repo/settings.go

This file has not been changed.

+31
knotserver/db/collaborators.go
··· 1 + package db 2 + 3 + func (d *DB) AddCollaborator(ownerDid, rkey, subjectDid, didSlashRepo string) error { 4 + _, err := d.db.Exec( 5 + `insert or replace into collaborators (owner_did, rkey, subject_did, did_slash_repo) values (?, ?, ?, ?)`, 6 + ownerDid, rkey, subjectDid, didSlashRepo, 7 + ) 8 + return err 9 + } 10 + 11 + func (d *DB) RemoveCollaboratorByRkey(ownerDid, rkey string) (subjectDid, didSlashRepo string, err error) { 12 + row := d.db.QueryRow( 13 + `select subject_did, did_slash_repo from collaborators where owner_did = ? and rkey = ?`, 14 + ownerDid, rkey, 15 + ) 16 + err = row.Scan(&subjectDid, &didSlashRepo) 17 + if err != nil { 18 + return "", "", err 19 + } 20 + _, err = d.db.Exec(`delete from collaborators where owner_did = ? and rkey = ?`, ownerDid, rkey) 21 + return subjectDid, didSlashRepo, err 22 + } 23 + 24 + func (d *DB) CountCollaboratorRepos(subjectDid string) (int, error) { 25 + var n int 26 + err := d.db.QueryRow( 27 + `select count(*) from collaborators where subject_did = ?`, 28 + subjectDid, 29 + ).Scan(&n) 30 + return n, err 31 + }
+8
knotserver/db/db.go
··· 43 43 did text primary key 44 44 ); 45 45 46 + create table if not exists collaborators ( 47 + owner_did text not null, 48 + rkey text not null, 49 + subject_did text not null, 50 + did_slash_repo text not null, 51 + primary key (owner_did, rkey) 52 + ); 53 + 46 54 create table if not exists public_keys ( 47 55 id integer primary key autoincrement, 48 56 did text not null,
+43 -1
knotserver/ingester.go
··· 273 273 return err 274 274 } 275 275 276 + rkey := event.Commit.RKey 277 + if err := h.db.AddCollaborator(did, rkey, subjectId.DID.String(), didSlashRepo); err != nil { 278 + return err 279 + } 280 + 276 281 return h.fetchAndAddKeys(ctx, subjectId.DID.String()) 277 282 } 278 283 284 + func (h *Knot) processCollaboratorDelete(ctx context.Context, event *models.Event) error { 285 + l := log.FromContext(ctx) 286 + ownerDid := event.Did 287 + rkey := event.Commit.RKey 288 + 289 + subjectDid, didSlashRepo, err := h.db.RemoveCollaboratorByRkey(ownerDid, rkey) 290 + if err != nil { 291 + l.Debug("collaborator not in DB (may not have been on this knot)", "owner", ownerDid, "rkey", rkey, "err", err) 292 + return nil 293 + } 294 + 295 + if err := h.e.RemoveCollaborator(subjectDid, rbac.ThisServer, didSlashRepo); err != nil { 296 + l.Error("failed to remove collaborator from RBAC", "err", err) 297 + return err 298 + } 299 + 300 + if h.e.HasAnyPermissionInDomain(subjectDid, rbac.ThisServer) { 301 + return nil 302 + } 303 + 304 + if err := h.db.RemovePublicKey(subjectDid); err != nil { 305 + l.Error("failed to remove public keys", "did", subjectDid, "err", err) 306 + return err 307 + } 308 + if err := h.db.RemoveDid(subjectDid); err != nil { 309 + return err 310 + } 311 + h.jc.RemoveDid(subjectDid) 312 + l.Info("removed collaborator and revoked keys", "subject", subjectDid) 313 + return nil 314 + } 315 + 279 316 func (h *Knot) fetchAndAddKeys(ctx context.Context, did string) error { 280 317 l := log.FromContext(ctx) 281 318 ··· 341 378 case tangled.RepoPullNSID: 342 379 err = h.processPull(ctx, event) 343 380 case tangled.RepoCollaboratorNSID: 344 - err = h.processCollaborator(ctx, event) 381 + switch event.Commit.Operation { 382 + case models.CommitOperationCreate, models.CommitOperationUpdate: 383 + err = h.processCollaborator(ctx, event) 384 + case models.CommitOperationDelete: 385 + err = h.processCollaboratorDelete(ctx, event) 386 + } 345 387 } 346 388 347 389 if err != nil {
+9
rbac/rbac.go
··· 305 305 return e.E.Enforce(user, domain, repo, "repo:invite") 306 306 } 307 307 308 + func (e *Enforcer) HasAnyPermissionInDomain(user, domain string) bool { 309 + perms := e.E.GetPermissionsForUserInDomain(user, domain) 310 + return len(perms) > 0 311 + } 312 + 313 + func (e *Enforcer) HasAnyPermissionInSpindle(user, domain string) bool { 314 + return e.HasAnyPermissionInDomain(user, intoSpindle(domain)) 315 + } 316 + 308 317 // given a repo, what permissions does this user have? repo:owner? repo:invite? etc. 309 318 func (e *Enforcer) GetPermissionsInRepo(user, domain, repo string) []string { 310 319 var permissions []string
+22
spindle/db/collaborators.go
··· 1 + package db 2 + 3 + func (d *DB) AddCollaborator(ownerDid, rkey, subjectDid, didSlashRepo string) error { 4 + _, err := d.Exec( 5 + `insert or replace into collaborators (owner_did, rkey, subject_did, did_slash_repo) values (?, ?, ?, ?)`, 6 + ownerDid, rkey, subjectDid, didSlashRepo, 7 + ) 8 + return err 9 + } 10 + 11 + func (d *DB) RemoveCollaboratorByRkey(ownerDid, rkey string) (subjectDid, didSlashRepo string, err error) { 12 + row := d.QueryRow( 13 + `select subject_did, did_slash_repo from collaborators where owner_did = ? and rkey = ?`, 14 + ownerDid, rkey, 15 + ) 16 + err = row.Scan(&subjectDid, &didSlashRepo) 17 + if err != nil { 18 + return "", "", err 19 + } 20 + _, err = d.Exec(`delete from collaborators where owner_did = ? and rkey = ?`, ownerDid, rkey) 21 + return subjectDid, didSlashRepo, err 22 + }
+8
spindle/db/db.go
··· 49 49 unique(owner, name) 50 50 ); 51 51 52 + create table if not exists collaborators ( 53 + owner_did text not null, 54 + rkey text not null, 55 + subject_did text not null, 56 + did_slash_repo text not null, 57 + primary key (owner_did, rkey) 58 + ); 59 + 52 60 create table if not exists spindle_members ( 53 61 -- identifiers for the record 54 62 id integer primary key autoincrement,
+30
spindle/ingester.go
··· 264 264 return fmt.Errorf("failed to add repo: %w", err) 265 265 } 266 266 267 + if err := s.db.AddCollaborator(owner.DID.String(), e.Commit.RKey, record.Subject, didSlashRepo); err != nil { 268 + return err 269 + } 270 + 271 + return nil 272 + 273 + case models.CommitOperationDelete: 274 + ownerDid := e.Did 275 + rkey := e.Commit.RKey 276 + 277 + subjectDid, didSlashRepo, err := s.db.RemoveCollaboratorByRkey(ownerDid, rkey) 278 + if err != nil { 279 + l.Debug("collaborator not in DB (may not have been on this spindle)", "owner", ownerDid, "rkey", rkey, "err", err) 280 + return nil 281 + } 282 + 283 + if err := s.e.RemoveCollaborator(subjectDid, rbac.ThisServer, didSlashRepo); err != nil { 284 + l.Error("failed to remove collaborator from RBAC", "err", err) 285 + return err 286 + } 287 + 288 + if s.e.HasAnyPermissionInSpindle(subjectDid, rbacDomain) { 289 + return nil 290 + } 291 + 292 + if err := s.db.RemoveDid(subjectDid); err != nil { 293 + return err 294 + } 295 + s.jc.RemoveDid(subjectDid) 296 + l.Info("removed collaborator", "subject", subjectDid) 267 297 return nil 268 298 } 269 299 return nil

History

2 rounds 4 comments
sign up or login to add to the discussion
1 commit
expand
feat(repo): add collaborator removal
no conflicts, ready to merge
expand 1 comment

added collaborators table to keep track in knotserver and spindle

1 commit
expand
feat(repo): add collaborator removal
expand 3 comments

thanks for the contribution! this PR does not handle all the scenarios:

  • the collaborator should be removed from the knot
  • their key must be removed from the knot if they don't have any other collaborator/membership relationship with the knot
  • likewise with spindles
  • nice catch about removing keys if no other relationship exist, will dig into this

lovely, thanks for picking this up. the tricky bit is that knots presently do not hold rkeys in their DBs, and embedded tap would help us backfill and achieve this easily.