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

appview: split off spindle verification logic into separate pkg

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 50cc4da6 460e1816

verified
Changed files
+560 -266
appview
spindleresolver
spindles
spindleverify
-176
appview/spindleresolver/resolver.go
···
-
package spindleresolver
-
-
import (
-
"context"
-
"encoding/json"
-
"errors"
-
"fmt"
-
"io"
-
"net/http"
-
"strings"
-
"time"
-
-
"tangled.sh/tangled.sh/core/api/tangled"
-
"tangled.sh/tangled.sh/core/appview/cache"
-
"tangled.sh/tangled.sh/core/appview/idresolver"
-
-
"github.com/bluesky-social/indigo/api/atproto"
-
"github.com/bluesky-social/indigo/xrpc"
-
)
-
-
type ResolutionStatus string
-
-
const (
-
StatusOK ResolutionStatus = "ok"
-
StatusError ResolutionStatus = "error"
-
StatusInvalid ResolutionStatus = "invalid"
-
)
-
-
type Resolution struct {
-
Status ResolutionStatus `json:"status"`
-
OwnerDID string `json:"ownerDid,omitempty"`
-
VerifiedAt time.Time `json:"verifiedAt"`
-
}
-
-
type Resolver struct {
-
cache *cache.Cache
-
http *http.Client
-
config Config
-
idResolver *idresolver.Resolver
-
}
-
-
type Config struct {
-
HitTTL time.Duration
-
ErrTTL time.Duration
-
InvalidTTL time.Duration
-
Dev bool
-
}
-
-
func NewResolver(cache *cache.Cache, client *http.Client, config Config) *Resolver {
-
if client == nil {
-
client = &http.Client{
-
Timeout: 2 * time.Second,
-
}
-
}
-
return &Resolver{
-
cache: cache,
-
http: client,
-
config: config,
-
}
-
}
-
-
func DefaultResolver(cache *cache.Cache) *Resolver {
-
return NewResolver(
-
cache,
-
&http.Client{
-
Timeout: 2 * time.Second,
-
},
-
Config{
-
HitTTL: 24 * time.Hour,
-
ErrTTL: 30 * time.Second,
-
InvalidTTL: 1 * time.Minute,
-
},
-
)
-
}
-
-
func (r *Resolver) ResolveInstance(ctx context.Context, domain string) (*Resolution, error) {
-
key := "spindle:" + domain
-
-
val, err := r.cache.Get(ctx, key).Result()
-
if err == nil {
-
var cached Resolution
-
if err := json.Unmarshal([]byte(val), &cached); err == nil {
-
return &cached, nil
-
}
-
}
-
-
resolution, ttl := r.verify(ctx, domain)
-
-
data, _ := json.Marshal(resolution)
-
r.cache.Set(ctx, key, data, ttl)
-
-
if resolution.Status == StatusOK {
-
return resolution, nil
-
}
-
-
return resolution, fmt.Errorf("verification failed: %s", resolution.Status)
-
}
-
-
func (r *Resolver) verify(ctx context.Context, domain string) (*Resolution, time.Duration) {
-
owner, err := r.fetchOwner(ctx, domain)
-
if err != nil {
-
return &Resolution{Status: StatusError, VerifiedAt: time.Now()}, r.config.ErrTTL
-
}
-
-
record, err := r.fetchRecord(ctx, owner, domain)
-
if err != nil {
-
return &Resolution{Status: StatusError, VerifiedAt: time.Now()}, r.config.ErrTTL
-
}
-
-
if record.Instance == domain {
-
return &Resolution{
-
Status: StatusOK,
-
OwnerDID: owner,
-
VerifiedAt: time.Now(),
-
}, r.config.HitTTL
-
}
-
-
return &Resolution{
-
Status: StatusInvalid,
-
OwnerDID: owner,
-
VerifiedAt: time.Now(),
-
}, r.config.InvalidTTL
-
}
-
-
func (r *Resolver) fetchOwner(ctx context.Context, domain string) (string, error) {
-
scheme := "https"
-
if r.config.Dev {
-
scheme = "http"
-
}
-
-
url := fmt.Sprintf("%s://%s/owner", scheme, domain)
-
req, err := http.NewRequest("GET", url, nil)
-
if err != nil {
-
return "", err
-
}
-
-
resp, err := r.http.Do(req.WithContext(ctx))
-
if err != nil || resp.StatusCode != 200 {
-
return "", errors.New("failed to fetch /owner")
-
}
-
-
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) // read atmost 1kb of data
-
if err != nil {
-
return "", fmt.Errorf("failed to read /owner response: %w", err)
-
}
-
-
did := strings.TrimSpace(string(body))
-
if did == "" {
-
return "", errors.New("empty DID in /owner response")
-
}
-
-
return did, nil
-
}
-
-
func (r *Resolver) fetchRecord(ctx context.Context, did, rkey string) (*tangled.Spindle, error) {
-
ident, err := r.idResolver.ResolveIdent(ctx, did)
-
if err != nil {
-
return nil, err
-
}
-
-
xrpcc := xrpc.Client{
-
Host: ident.PDSEndpoint(),
-
}
-
-
rec, err := atproto.RepoGetRecord(ctx, &xrpcc, "", tangled.SpindleNSID, did, rkey)
-
if err != nil {
-
return nil, err
-
}
-
-
out, ok := rec.Value.Val.(*tangled.Spindle)
-
if !ok {
-
return nil, fmt.Errorf("invalid record returned")
-
}
-
-
return out, nil
-
}
+442 -90
appview/spindles/spindles.go
···
package spindles
import (
-
"context"
"errors"
"fmt"
-
"io"
"log/slog"
"net/http"
-
"strings"
+
"slices"
"time"
"github.com/go-chi/chi/v5"
"tangled.sh/tangled.sh/core/api/tangled"
+
"tangled.sh/tangled.sh/core/appview"
"tangled.sh/tangled.sh/core/appview/config"
"tangled.sh/tangled.sh/core/appview/db"
+
"tangled.sh/tangled.sh/core/appview/idresolver"
"tangled.sh/tangled.sh/core/appview/middleware"
"tangled.sh/tangled.sh/core/appview/oauth"
"tangled.sh/tangled.sh/core/appview/pages"
+
verify "tangled.sh/tangled.sh/core/appview/spindleverify"
"tangled.sh/tangled.sh/core/rbac"
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
)
type Spindles struct {
-
Db *db.DB
-
OAuth *oauth.OAuth
-
Pages *pages.Pages
-
Config *config.Config
-
Enforcer *rbac.Enforcer
-
Logger *slog.Logger
+
Db *db.DB
+
OAuth *oauth.OAuth
+
Pages *pages.Pages
+
Config *config.Config
+
Enforcer *rbac.Enforcer
+
IdResolver *idresolver.Resolver
+
Logger *slog.Logger
}
func (s *Spindles) Router() http.Handler {
r := chi.NewRouter()
-
r.Use(middleware.AuthMiddleware(s.OAuth))
+
r.With(middleware.AuthMiddleware(s.OAuth)).Get("/", s.spindles)
+
r.With(middleware.AuthMiddleware(s.OAuth)).Post("/register", s.register)
-
r.Get("/", s.spindles)
-
r.Post("/register", s.register)
-
r.Delete("/{instance}", s.delete)
-
r.Post("/{instance}/retry", s.retry)
+
r.With(middleware.AuthMiddleware(s.OAuth)).Get("/{instance}", s.dashboard)
+
r.With(middleware.AuthMiddleware(s.OAuth)).Delete("/{instance}", s.delete)
+
+
r.With(middleware.AuthMiddleware(s.OAuth)).Post("/{instance}/retry", s.retry)
+
r.With(middleware.AuthMiddleware(s.OAuth)).Post("/{instance}/add", s.addMember)
+
r.With(middleware.AuthMiddleware(s.OAuth)).Post("/{instance}/remove", s.removeMember)
return r
}
···
})
}
+
func (s *Spindles) dashboard(w http.ResponseWriter, r *http.Request) {
+
l := s.Logger.With("handler", "dashboard")
+
+
user := s.OAuth.GetUser(r)
+
l = l.With("user", user.Did)
+
+
instance := chi.URLParam(r, "instance")
+
if instance == "" {
+
return
+
}
+
l = l.With("instance", instance)
+
+
spindles, err := db.GetSpindles(
+
s.Db,
+
db.FilterEq("instance", instance),
+
db.FilterEq("owner", user.Did),
+
db.FilterIsNot("verified", "null"),
+
)
+
if err != nil || len(spindles) != 1 {
+
l.Error("failed to get spindle", "err", err, "len(spindles)", len(spindles))
+
http.Error(w, "Not found", http.StatusNotFound)
+
return
+
}
+
+
spindle := spindles[0]
+
members, err := s.Enforcer.GetSpindleUsersByRole("server:member", spindle.Instance)
+
if err != nil {
+
l.Error("failed to get spindle members", "err", err)
+
http.Error(w, "Not found", http.StatusInternalServerError)
+
return
+
}
+
slices.Sort(members)
+
+
repos, err := db.GetRepos(
+
s.Db,
+
db.FilterEq("spindle", instance),
+
)
+
if err != nil {
+
l.Error("failed to get spindle repos", "err", err)
+
http.Error(w, "Not found", http.StatusInternalServerError)
+
return
+
}
+
+
identsToResolve := make([]string, len(members))
+
for i, member := range members {
+
identsToResolve[i] = member
+
}
+
resolvedIds := s.IdResolver.ResolveIdents(r.Context(), identsToResolve)
+
didHandleMap := make(map[string]string)
+
for _, identity := range resolvedIds {
+
if !identity.Handle.IsInvalidHandle() {
+
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
+
} else {
+
didHandleMap[identity.DID.String()] = identity.DID.String()
+
}
+
}
+
+
// organize repos by did
+
repoMap := make(map[string][]db.Repo)
+
for _, r := range repos {
+
repoMap[r.Did] = append(repoMap[r.Did], r)
+
}
+
+
s.Pages.SpindleDashboard(w, pages.SpindleDashboardParams{
+
LoggedInUser: user,
+
Spindle: spindle,
+
Members: members,
+
Repos: repoMap,
+
DidHandleMap: didHandleMap,
+
})
+
}
+
// this endpoint inserts a record on behalf of the user to register that domain
//
// when registered, it also makes a request to see if the spindle declares this users as its owner,
···
s.Pages.Notice(w, noticeId, "Incomplete form.")
return
}
+
l = l.With("instance", instance)
+
l = l.With("user", user.Did)
tx, err := s.Db.Begin()
if err != nil {
···
fail()
return
}
-
defer tx.Rollback()
+
defer func() {
+
tx.Rollback()
+
s.Enforcer.E.LoadPolicy()
+
}()
err = db.AddSpindle(tx, db.Spindle{
Owner: syntax.DID(user.Did),
···
return
}
+
err = s.Enforcer.AddSpindle(instance)
+
if err != nil {
+
l.Error("failed to create spindle", "err", err)
+
fail()
+
return
+
}
+
// create record on pds
client, err := s.OAuth.AuthorizedClient(r)
if err != nil {
···
return
}
-
// begin verification
-
expectedOwner, err := fetchOwner(r.Context(), instance, s.Config.Core.Dev)
-
if err != nil {
-
l.Error("verification failed", "err", err)
-
-
// just refresh the page
-
s.Pages.HxRefresh(w)
-
return
-
}
-
-
if expectedOwner != user.Did {
-
// verification failed
-
l.Error("verification failed", "expectedOwner", expectedOwner, "observedOwner", user.Did)
-
s.Pages.HxRefresh(w)
-
return
-
}
-
-
tx, err = s.Db.Begin()
-
if err != nil {
-
l.Error("failed to commit verification info", "err", err)
-
s.Pages.HxRefresh(w)
-
return
-
}
-
defer func() {
-
tx.Rollback()
-
s.Enforcer.E.LoadPolicy()
-
}()
-
-
// mark this spindle as verified in the db
-
_, err = db.VerifySpindle(
-
tx,
-
db.FilterEq("owner", user.Did),
-
db.FilterEq("instance", instance),
-
)
-
-
err = s.Enforcer.AddSpindleOwner(instance, user.Did)
+
err = s.Enforcer.E.SavePolicy()
if err != nil {
l.Error("failed to update ACL", "err", err)
s.Pages.HxRefresh(w)
return
}
-
err = tx.Commit()
+
// begin verification
+
err = verify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev)
if err != nil {
-
l.Error("failed to commit verification info", "err", err)
+
l.Error("verification failed", "err", err)
s.Pages.HxRefresh(w)
return
}
-
err = s.Enforcer.E.SavePolicy()
+
_, err = verify.MarkVerified(s.Db, s.Enforcer, instance, user.Did)
if err != nil {
-
l.Error("failed to update ACL", "err", err)
+
l.Error("failed to mark verified", "err", err)
s.Pages.HxRefresh(w)
return
}
···
func (s *Spindles) delete(w http.ResponseWriter, r *http.Request) {
user := s.OAuth.GetUser(r)
-
l := s.Logger.With("handler", "register")
+
l := s.Logger.With("handler", "delete")
noticeId := "operation-error"
defaultErr := "Failed to delete spindle. Try again later."
···
return
}
+
spindles, err := db.GetSpindles(
+
s.Db,
+
db.FilterEq("owner", user.Did),
+
db.FilterEq("instance", instance),
+
)
+
if err != nil || len(spindles) != 1 {
+
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
+
fail()
+
return
+
}
+
+
if string(spindles[0].Owner) != user.Did {
+
l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner)
+
s.Pages.Notice(w, noticeId, "Failed to delete spindle, unauthorized deletion attempt.")
+
return
+
}
+
tx, err := s.Db.Begin()
if err != nil {
l.Error("failed to start txn", "err", err)
fail()
return
}
-
defer tx.Rollback()
+
defer func() {
+
tx.Rollback()
+
s.Enforcer.E.LoadPolicy()
+
}()
err = db.DeleteSpindle(
tx,
···
)
if err != nil {
l.Error("failed to delete spindle", "err", err)
+
fail()
+
return
+
}
+
+
err = s.Enforcer.RemoveSpindle(instance)
+
if err != nil {
+
l.Error("failed to update ACL", "err", err)
fail()
return
}
···
return
}
+
err = s.Enforcer.E.SavePolicy()
+
if err != nil {
+
l.Error("failed to update ACL", "err", err)
+
s.Pages.HxRefresh(w)
+
return
+
}
+
+
shouldRedirect := r.Header.Get("shouldRedirect")
+
if shouldRedirect == "true" {
+
s.Pages.HxRedirect(w, "/spindles")
+
return
+
}
+
w.Write([]byte{})
}
func (s *Spindles) retry(w http.ResponseWriter, r *http.Request) {
user := s.OAuth.GetUser(r)
-
l := s.Logger.With("handler", "register")
+
l := s.Logger.With("handler", "retry")
noticeId := "operation-error"
defaultErr := "Failed to verify spindle. Try again later."
···
fail()
return
}
+
l = l.With("instance", instance)
+
l = l.With("user", user.Did)
+
+
spindles, err := db.GetSpindles(
+
s.Db,
+
db.FilterEq("owner", user.Did),
+
db.FilterEq("instance", instance),
+
)
+
if err != nil || len(spindles) != 1 {
+
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
+
fail()
+
return
+
}
+
+
if string(spindles[0].Owner) != user.Did {
+
l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner)
+
s.Pages.Notice(w, noticeId, "Failed to verify spindle, unauthorized verification attempt.")
+
return
+
}
// begin verification
-
expectedOwner, err := fetchOwner(r.Context(), instance, s.Config.Core.Dev)
+
err = verify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev)
if err != nil {
l.Error("verification failed", "err", err)
+
+
if errors.Is(err, verify.FetchError) {
+
s.Pages.Notice(w, noticeId, err.Error())
+
return
+
}
+
+
if e, ok := err.(*verify.OwnerMismatch); ok {
+
s.Pages.Notice(w, noticeId, e.Error())
+
return
+
}
+
fail()
return
}
-
if expectedOwner != user.Did {
-
l.Error("verification failed", "expectedOwner", expectedOwner, "observedOwner", user.Did)
-
s.Pages.Notice(w, noticeId, fmt.Sprintf("Owner did not match, expected %s, got %s", expectedOwner, user.Did))
+
rowId, err := verify.MarkVerified(s.Db, s.Enforcer, instance, user.Did)
+
if err != nil {
+
l.Error("failed to mark verified", "err", err)
+
s.Pages.Notice(w, noticeId, err.Error())
return
}
-
// mark this spindle as verified in the db
-
rowId, err := db.VerifySpindle(
+
verifiedSpindle, err := db.GetSpindles(
+
s.Db,
+
db.FilterEq("id", rowId),
+
)
+
if err != nil || len(verifiedSpindle) != 1 {
+
l.Error("failed get new spindle", "err", err)
+
s.Pages.HxRefresh(w)
+
return
+
}
+
+
shouldRefresh := r.Header.Get("shouldRefresh")
+
if shouldRefresh == "true" {
+
s.Pages.HxRefresh(w)
+
return
+
}
+
+
w.Header().Set("HX-Reswap", "outerHTML")
+
s.Pages.SpindleListing(w, pages.SpindleListingParams{verifiedSpindle[0]})
+
}
+
+
func (s *Spindles) addMember(w http.ResponseWriter, r *http.Request) {
+
user := s.OAuth.GetUser(r)
+
l := s.Logger.With("handler", "addMember")
+
+
instance := chi.URLParam(r, "instance")
+
if instance == "" {
+
l.Error("empty instance")
+
http.Error(w, "Not found", http.StatusNotFound)
+
return
+
}
+
l = l.With("instance", instance)
+
l = l.With("user", user.Did)
+
+
spindles, err := db.GetSpindles(
s.Db,
db.FilterEq("owner", user.Did),
db.FilterEq("instance", instance),
)
+
if err != nil || len(spindles) != 1 {
+
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
+
http.Error(w, "Not found", http.StatusNotFound)
+
return
+
}
+
+
noticeId := fmt.Sprintf("add-member-error-%d", spindles[0].Id)
+
defaultErr := "Failed to add member. Try again later."
+
fail := func() {
+
s.Pages.Notice(w, noticeId, defaultErr)
+
}
+
+
if string(spindles[0].Owner) != user.Did {
+
l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner)
+
s.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.")
+
return
+
}
+
+
member := r.FormValue("member")
+
if member == "" {
+
l.Error("empty member")
+
s.Pages.Notice(w, noticeId, "Failed to add member, empty form.")
+
return
+
}
+
l = l.With("member", member)
+
+
memberId, err := s.IdResolver.ResolveIdent(r.Context(), member)
+
if err != nil {
+
l.Error("failed to resolve member identity to handle", "err", err)
+
s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.")
+
return
+
}
+
if memberId.Handle.IsInvalidHandle() {
+
l.Error("failed to resolve member identity to handle")
+
s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.")
+
return
+
}
+
+
// write to pds
+
client, err := s.OAuth.AuthorizedClient(r)
+
if err != nil {
+
l.Error("failed to authorize client", "err", err)
+
fail()
+
return
+
}
+
+
tx, err := s.Db.Begin()
if err != nil {
-
l.Error("verification failed", "err", err)
+
l.Error("failed to start txn", "err", err)
fail()
return
}
+
defer func() {
+
tx.Rollback()
+
s.Enforcer.E.LoadPolicy()
+
}()
-
verifiedSpindle := db.Spindle{
-
Id: int(rowId),
-
Owner: syntax.DID(user.Did),
+
rkey := appview.TID()
+
+
// add member to db
+
if err = db.AddSpindleMember(tx, db.SpindleMember{
+
Did: syntax.DID(user.Did),
+
Rkey: rkey,
Instance: instance,
+
Subject: memberId.DID,
+
}); err != nil {
+
l.Error("failed to add spindle member", "err", err)
+
fail()
+
return
}
-
w.Header().Set("HX-Reswap", "outerHTML")
-
s.Pages.SpindleListing(w, pages.SpindleListingParams{
-
LoggedInUser: user,
-
Spindle: verifiedSpindle,
+
if err = s.Enforcer.AddSpindleMember(instance, memberId.DID.String()); err != nil {
+
l.Error("failed to add member to ACLs")
+
fail()
+
return
+
}
+
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
+
Collection: tangled.SpindleMemberNSID,
+
Repo: user.Did,
+
Rkey: rkey,
+
Record: &lexutil.LexiconTypeDecoder{
+
Val: &tangled.SpindleMember{
+
CreatedAt: time.Now().Format(time.RFC3339),
+
Instance: instance,
+
Subject: memberId.DID.String(),
+
},
+
},
})
+
if err != nil {
+
l.Error("failed to add record to PDS", "err", err)
+
s.Pages.Notice(w, noticeId, "Failed to add record to PDS, try again later.")
+
return
+
}
+
+
if err = tx.Commit(); err != nil {
+
l.Error("failed to commit txn", "err", err)
+
fail()
+
return
+
}
+
+
if err = s.Enforcer.E.SavePolicy(); err != nil {
+
l.Error("failed to add member to ACLs", "err", err)
+
fail()
+
return
+
}
+
+
// success
+
s.Pages.HxRedirect(w, fmt.Sprintf("/spindles/%s", instance))
}
-
func fetchOwner(ctx context.Context, domain string, dev bool) (string, error) {
-
scheme := "https"
-
if dev {
-
scheme = "http"
+
func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) {
+
user := s.OAuth.GetUser(r)
+
l := s.Logger.With("handler", "removeMember")
+
+
noticeId := "operation-error"
+
defaultErr := "Failed to add member. Try again later."
+
fail := func() {
+
s.Pages.Notice(w, noticeId, defaultErr)
}
-
url := fmt.Sprintf("%s://%s/owner", scheme, domain)
-
req, err := http.NewRequest("GET", url, nil)
+
instance := chi.URLParam(r, "instance")
+
if instance == "" {
+
l.Error("empty instance")
+
fail()
+
return
+
}
+
l = l.With("instance", instance)
+
l = l.With("user", user.Did)
+
+
spindles, err := db.GetSpindles(
+
s.Db,
+
db.FilterEq("owner", user.Did),
+
db.FilterEq("instance", instance),
+
)
+
if err != nil || len(spindles) != 1 {
+
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
+
fail()
+
return
+
}
+
+
if string(spindles[0].Owner) != user.Did {
+
l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner)
+
s.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.")
+
return
+
}
+
+
member := r.FormValue("member")
+
if member == "" {
+
l.Error("empty member")
+
s.Pages.Notice(w, noticeId, "Failed to add member, empty form.")
+
return
+
}
+
l = l.With("member", member)
+
+
memberId, err := s.IdResolver.ResolveIdent(r.Context(), member)
+
if err != nil {
+
l.Error("failed to resolve member identity to handle", "err", err)
+
s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.")
+
return
+
}
+
if memberId.Handle.IsInvalidHandle() {
+
l.Error("failed to resolve member identity to handle")
+
s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.")
+
return
+
}
+
+
tx, err := s.Db.Begin()
if err != nil {
-
return "", err
+
l.Error("failed to start txn", "err", err)
+
fail()
+
return
+
}
+
defer func() {
+
tx.Rollback()
+
s.Enforcer.E.LoadPolicy()
+
}()
+
+
// get the record from the DB first:
+
members, err := db.GetSpindleMembers(
+
s.Db,
+
db.FilterEq("did", user.Did),
+
db.FilterEq("instance", instance),
+
db.FilterEq("subject", memberId.DID),
+
)
+
if err != nil || len(members) != 1 {
+
l.Error("failed to get member", "err", err)
+
fail()
+
return
}
-
client := &http.Client{
-
Timeout: 1 * time.Second,
+
// remove from db
+
if err = db.RemoveSpindleMember(
+
tx,
+
db.FilterEq("did", user.Did),
+
db.FilterEq("instance", instance),
+
db.FilterEq("subject", memberId.DID),
+
); err != nil {
+
l.Error("failed to remove spindle member", "err", err)
+
fail()
+
return
}
-
resp, err := client.Do(req.WithContext(ctx))
-
if err != nil || resp.StatusCode != 200 {
-
return "", errors.New("failed to fetch /owner")
+
// remove from enforcer
+
if err = s.Enforcer.RemoveSpindleMember(instance, memberId.DID.String()); err != nil {
+
l.Error("failed to update ACLs", "err", err)
+
fail()
+
return
}
-
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) // read atmost 1kb of data
+
client, err := s.OAuth.AuthorizedClient(r)
if err != nil {
-
return "", fmt.Errorf("failed to read /owner response: %w", err)
+
l.Error("failed to authorize client", "err", err)
+
fail()
+
return
}
-
did := strings.TrimSpace(string(body))
-
if did == "" {
-
return "", errors.New("empty DID in /owner response")
+
// remove from pds
+
_, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
+
Collection: tangled.SpindleMemberNSID,
+
Repo: user.Did,
+
Rkey: members[0].Rkey,
+
})
+
if err != nil {
+
// non-fatal
+
l.Error("failed to delete record", "err", err)
+
}
+
+
// commit everything
+
if err = tx.Commit(); err != nil {
+
l.Error("failed to commit txn", "err", err)
+
fail()
+
return
}
-
return did, nil
+
// commit everything
+
if err = s.Enforcer.E.SavePolicy(); err != nil {
+
l.Error("failed to save ACLs", "err", err)
+
fail()
+
return
+
}
+
+
// ok
+
s.Pages.HxRefresh(w)
+
return
}
+118
appview/spindleverify/verify.go
···
+
package spindleverify
+
+
import (
+
"context"
+
"errors"
+
"fmt"
+
"io"
+
"net/http"
+
"strings"
+
"time"
+
+
"tangled.sh/tangled.sh/core/appview/db"
+
"tangled.sh/tangled.sh/core/rbac"
+
)
+
+
var (
+
FetchError = errors.New("failed to fetch owner")
+
)
+
+
// TODO: move this to "spindleclient" or similar
+
func fetchOwner(ctx context.Context, domain string, dev bool) (string, error) {
+
scheme := "https"
+
if dev {
+
scheme = "http"
+
}
+
+
url := fmt.Sprintf("%s://%s/owner", scheme, domain)
+
req, err := http.NewRequest("GET", url, nil)
+
if err != nil {
+
return "", err
+
}
+
+
client := &http.Client{
+
Timeout: 1 * time.Second,
+
}
+
+
resp, err := client.Do(req.WithContext(ctx))
+
if err != nil || resp.StatusCode != 200 {
+
return "", fmt.Errorf("failed to fetch /owner")
+
}
+
+
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) // read atmost 1kb of data
+
if err != nil {
+
return "", fmt.Errorf("failed to read /owner response: %w", err)
+
}
+
+
did := strings.TrimSpace(string(body))
+
if did == "" {
+
return "", fmt.Errorf("empty DID in /owner response")
+
}
+
+
return did, nil
+
}
+
+
type OwnerMismatch struct {
+
expected string
+
observed string
+
}
+
+
func (e *OwnerMismatch) Error() string {
+
return fmt.Sprintf("owner mismatch: %q != %q", e.expected, e.observed)
+
}
+
+
func RunVerification(ctx context.Context, instance, expectedOwner string, dev bool) error {
+
// begin verification
+
observedOwner, err := fetchOwner(ctx, instance, dev)
+
if err != nil {
+
return fmt.Errorf("%w: %w", FetchError, err)
+
}
+
+
if observedOwner != expectedOwner {
+
return &OwnerMismatch{
+
expected: expectedOwner,
+
observed: observedOwner,
+
}
+
}
+
+
return nil
+
}
+
+
// mark this spindle as verified in the DB and add this user as its owner
+
func MarkVerified(d *db.DB, e *rbac.Enforcer, instance, owner string) (int64, error) {
+
tx, err := d.Begin()
+
if err != nil {
+
return 0, fmt.Errorf("failed to create txn: %w", err)
+
}
+
defer func() {
+
tx.Rollback()
+
e.E.LoadPolicy()
+
}()
+
+
// mark this spindle as verified in the db
+
rowId, err := db.VerifySpindle(
+
tx,
+
db.FilterEq("owner", owner),
+
db.FilterEq("instance", instance),
+
)
+
if err != nil {
+
return 0, fmt.Errorf("failed to write to DB: %w", err)
+
}
+
+
err = e.AddSpindleOwner(instance, owner)
+
if err != nil {
+
return 0, fmt.Errorf("failed to update ACL: %w", err)
+
}
+
+
err = tx.Commit()
+
if err != nil {
+
return 0, fmt.Errorf("failed to commit txn: %w", err)
+
}
+
+
err = e.E.SavePolicy()
+
if err != nil {
+
return 0, fmt.Errorf("failed to update ACL: %w", err)
+
}
+
+
return rowId, nil
+
}