clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

tighten tenant service authorization and delivery wiring

Dami committed fbc0c60
commit
fbc0c60c00d62e508d148d64cc100102a8b3c004
parent
ee88d2a06e952638cbb3f479ed06d9bb94d58d2e
author
Dami <dami.etoile@gmail.com>
1 file changed, +88 -10
Minternal/daemon/services.go98+++++++++++++++++++++++++++---
M internal/daemon/services.go +88 -10
@@ -1,12 +1,15 @@
11 package daemon22 33 import (4+ "bytes"45 "context"56 "encoding/json"7+ "errors"68 "io"79 "net/http"810 "net/url"911 "strings"12+ "time"1013 1114 "github.com/FelineStateMachine/tinyrelay/internal/blob"1215 "github.com/FelineStateMachine/tinyrelay/internal/configport"
@@ -27,18 +30,38 @@ func (t *Tenant) initServices(ctx context.Context) error {
2730 var err error2831 t.blobs, err = blob.New(ctx, blob.Config{2932 Root: t.meta.Paths.Root, Store: t.store, Authorize: t.authorizeBlob,30- CanRead: func(ctx context.Context, pubkey string, hashes []string) bool {33+ CanRead: func(ctx context.Context, hash string, pubkeys []string) bool {3134 p := t.Policy()3235 if p.Reads == "open" {3336 return true3437 }35- role, err := t.community.Role(ctx, pubkey)36- return err == nil && role != ""38+ for _, pubkey := range pubkeys {39+ if p.Reads == "auth" {40+ return true41+ }42+ role, err := t.community.Role(ctx, pubkey)43+ if err == nil && role != "" {44+ return true45+ }46+ }47+ return false3748 }, IsOwner: func(pubkey string) bool { return pubkey == t.Policy().Owner },3849 })3950 if err != nil {4051 return err4152 }53+ t.community.ConfigurePolicy(t.Policy)54+ t.community.OnMembershipApplied(func(event.Event) {55+ go func() {56+ generated, err := t.records.PublishMembership(context.Background(), time.Now().Unix())57+ if err != nil {58+ return59+ }60+ for _, record := range generated {61+ _ = t.generatedRecord(context.Background(), record)62+ }63+ }()64+ })4265 t.sites, err = sites.New(sites.Config{4366 Store: t.store,4467 GetBlob: func(ctx context.Context, hash string) (sites.Blob, error) {
@@ -62,10 +85,10 @@ func (t *Tenant) initServices(ctx context.Context) error {
6285 }6386 t.config = configport.New(configport.ConfigStore{Store: t.store, Community: t.community, Policy: t.Policy, OnApplied: func(p policy.Policy) { t.replacePolicy(p) }})6487 transport := &replication.NostrTransport{Dialer: replication.WebsocketDialer{}}65- t.replication, err = replication.NewService(replication.Config{Store: t.store, Policy: replication.Policy{Enabled: t.Policy().Delivery.Enabled, SelfPubKey: t.Policy().Owner}, CurrentPolicy: func() replication.Policy {88+ t.replication, err = replication.NewService(replication.Config{Store: t.store, Policy: replication.Policy{Enabled: t.Policy().Delivery.Enabled, SelfPubKey: t.records.PublicKey()}, CurrentPolicy: func() replication.Policy {6689 p := t.Policy()67- return replication.Policy{Enabled: p.Delivery.Enabled, SelfPubKey: p.Owner}68- }, Delivery: transport, Pull: transport, Push: transport, DataDir: t.meta.Paths.Root, Ingest: t.ingest})90+ return replication.Policy{Enabled: p.Delivery.Enabled, ReadMembersOnly: p.Reads != "open", SelfPubKey: t.records.PublicKey()}91+ }, Directory: localDirectory{store: t.store}, Delivery: transport, Pull: transport, Push: transport, DataDir: t.meta.Paths.Root, Ingest: t.ingest})6992 if err != nil {7093 return err7194 }
@@ -86,6 +109,25 @@ func (t *Tenant) initServices(ctx context.Context) error {
86109 // backend adapts the management method signature to webui's form-oriented API.87110 type backend struct{ tenant *Tenant }88111 112+type localDirectory struct{ store *storage.Store }113+114+func (d localDirectory) WriteRelays(pubkey string) []string { return d.relays(pubkey, true) }115+func (d localDirectory) ReadRelays(pubkey string) []string { return d.relays(pubkey, false) }116+func (d localDirectory) relays(pubkey string, write bool) []string {117+ rows, err := d.store.Query(context.Background(), event.Filter{Authors: []string{pubkey}, Kinds: []int{10002}, Tags: map[string][]string{}}, storage.QueryOptions{Now: time.Now().Unix(), Access: storage.Access{All: true}, Limit: 1})118+ if err != nil || len(rows.Events) == 0 {119+ return nil120+ }121+ result := make([]string, 0)122+ for _, tag := range rows.Events[0].Tags {123+ if len(tag) < 2 || tag[0] != "r" || (!write && len(tag) > 2 && tag[2] != "read") || (write && len(tag) > 2 && tag[2] == "read") {124+ continue125+ }126+ result = append(result, tag[1])127+ }128+ return result129+}130+89131 func (b backend) Query(ctx context.Context, method string, params []json.RawMessage, actor string) (any, error) {90132 return b.tenant.Execute(ctx, actor, method, params)91133 }
@@ -121,11 +163,42 @@ func (t *Tenant) siteReadAccess(r *http.Request) bool {
121163 }122164 func (t *Tenant) authorizeBlob(r *http.Request, action blob.Action) (string, error) {123165 e, err := t.auth.WhoAsks(r.Header.Get("Authorization"), t.publicURL+r.URL.RequestURI(), r.Method, "", string(action), strings.TrimPrefix(r.URL.Path, "/"))124- return first(e), err166+ pubkey := first(e)167+ if err != nil {168+ return "", err169+ }170+ if pubkey == "" {171+ return "", errors.New("auth-required: blob authorization required")172+ }173+ if action == blob.ActionUpload || action == blob.ActionMirror {174+ if !t.Policy().Features.Files {175+ return "", errors.New("restricted: file uploads are disabled")176+ }177+ role, roleErr := t.community.Role(r.Context(), pubkey)178+ if roleErr != nil || (t.Policy().Writes != "open" && role == "") {179+ return "", errors.New("restricted: file upload is not allowed")180+ }181+ }182+ return pubkey, nil125183 }126184 func (t *Tenant) authorizeGit(ctx context.Context, r *http.Request, repo gitrelay.Repository) error {127- _, err := t.auth.VerifyNIP98(r.Header.Get("Authorization"), t.publicURL+r.URL.RequestURI(), r.Method, "")128- return err185+ body, err := io.ReadAll(r.Body)186+ if err != nil {187+ return err188+ }189+ r.Body = io.NopCloser(bytes.NewReader(body))190+ e, err := t.auth.VerifyNIP98(r.Header.Get("Authorization"), t.publicURL+r.URL.RequestURI(), r.Method, string(body))191+ if err != nil {192+ return err193+ }194+ if e.PubKey == repo.Owner {195+ return nil196+ }197+ role, roleErr := t.community.Role(ctx, e.PubKey)198+ if roleErr != nil || (role != "owner" && role != "moderator" && role != "member") {199+ return errors.New("restricted: repository authorization required")200+ }201+ return nil129202 }130203 func first(v []string) string {131204 if len(v) == 0 {
@@ -134,7 +207,12 @@ func first(v []string) string {
134207 return v[0]135208 }136209 func (t *Tenant) resolveUIActor(r *http.Request) (string, error) {137- e, err := t.auth.VerifyNIP98(r.Header.Get("Authorization"), t.publicURL+r.URL.RequestURI(), r.Method, "")210+ body, err := io.ReadAll(r.Body)211+ if err != nil {212+ return "", err213+ }214+ r.Body = io.NopCloser(bytes.NewReader(body))215+ e, err := t.auth.VerifyNIP98(r.Header.Get("Authorization"), t.publicURL+r.URL.RequestURI(), r.Method, string(body))138216 if err != nil {139217 return "", err140218 }