clone

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

feat/standalone-relay
tinyrelay / internal / daemon / private_grasp_test.go
private_grasp_test.go 6.0K
1package daemon23import (4 "context"5 "crypto/sha256"6 "encoding/base64"7 "encoding/hex"8 "encoding/json"9 "net/http"10 "net/http/httptest"11 "strings"12 "testing"13 "time"1415 "github.com/FelineStateMachine/tinyrelay/internal/event"16 "github.com/FelineStateMachine/tinyrelay/internal/policy"17)1819func TestPrivateProfileRedactsTenantMetadata(t *testing.T) {20 tenant := &Tenant{policy: policy.Defaults(strings.Repeat("a", 64))}21 tenant.policy.Features.Grasp = true22 tenant.policy.Features.Grasp08 = true23 tenant.policy.Reads = "members"24 tenant.policy.Name = "secret-project"25 tenant.policy.Description = "do not expose"26 profile := tenant.PrivateProfile()27 if !profile.Enabled || profile.Protocol != "GRASP-08" || profile.Reads != "members" {28 t.Fatalf("unexpected profile: %#v", profile)29 }30 if len(profile.Authentication) != 2 || profile.Authentication[0] != "NIP-42" || profile.Authentication[1] != "NIP-98" {31 t.Fatalf("unexpected auth methods: %#v", profile.Authentication)32 }33 info := tenant.privateNIP11()34 if info["name"] == tenant.policy.Name || info["description"] == tenant.policy.Description {35 t.Fatalf("private NIP-11 leaked tenant metadata: %#v", info)36 }37 if info["private_service"] != true {38 t.Fatalf("private service marker missing: %#v", info)39 }40}4142func TestPrivateAccessDisabledIsNoop(t *testing.T) {43 tenant := &Tenant{policy: policy.Defaults(strings.Repeat("a", 64))}44 access, err := tenant.PrivateAccess(context.Background(), "outsider")45 if err != nil || access.Allowed() {46 t.Fatalf("disabled private access = %#v, %v", access, err)47 }48}4950func TestPrivatePolicyShape(t *testing.T) {51 p := policy.Defaults(strings.Repeat("a", 64))52 p.Features.Grasp08 = true53 p.Features.Grasp = true54 if privatePolicy(p) {55 t.Fatal("open private policy recognized")56 }57 p.Reads = "members"58 if !privatePolicy(p) {59 t.Fatal("members-only GRASP-08 policy not recognized")60 }61}6263func TestPrivateAccessFollowsMembershipRevocation(t *testing.T) {64 _, tenant := testTenant(t)65 ctx := context.Background()66 p := tenant.Policy()67 p.Features.Grasp = true68 p.Features.Grasp08 = true69 p.Reads = "members"70 if err := tenant.applyPolicy(ctx, p); err != nil {71 t.Fatal(err)72 }73 memberSecret := strings.Repeat("2", 64)74 member, err := event.PublicKey(memberSecret)75 if err != nil {76 t.Fatal(err)77 }78 if _, err := tenant.community.Execute(ctx, p.Owner, "setmember", []json.RawMessage{json.RawMessage(`"` + member + `"`), json.RawMessage(`{"name":"reader"}`)}); err != nil {79 t.Fatal(err)80 }81 access, err := tenant.PrivateAccess(ctx, member)82 if err != nil || !access.Allowed() || !access.Member || access.Owner {83 t.Fatalf("member access = %#v, %v", access, err)84 }85 if _, err := tenant.community.Execute(ctx, p.Owner, "removemember", []json.RawMessage{json.RawMessage(`"` + member + `"`)}); err != nil {86 t.Fatal(err)87 }88 if _, err := tenant.PrivateAccess(ctx, member); err == nil {89 t.Fatal("revoked member retained private access")90 }91}9293func TestPrivateNIP11OmitsTenantPresentation(t *testing.T) {94 _, tenant := testTenant(t)95 p := tenant.Policy()96 p.Features.Grasp = true97 p.Features.Grasp08 = true98 p.Reads = "members"99 p.Name = "secret-name"100 p.Description = "secret-description"101 if err := tenant.applyPolicy(context.Background(), p); err != nil {102 t.Fatal(err)103 }104 req := httptest.NewRequest(http.MethodGet, "http://relay.test/", nil)105 req.Header.Set("Accept", "application/nostr+json")106 res := httptest.NewRecorder()107 tenant.ServeHTTP(res, req)108 if res.Code != http.StatusOK {109 t.Fatalf("NIP-11 status=%d body=%s", res.Code, res.Body.String())110 }111 body := res.Body.String()112 if strings.Contains(body, "secret-name") || strings.Contains(body, "secret-description") {113 t.Fatalf("private NIP-11 leaked presentation metadata: %s", body)114 }115 if !strings.Contains(body, "GRASP-08") || !strings.Contains(body, "private_service") {116 t.Fatalf("private NIP-11 omitted service profile: %s", body)117 }118 nip05 := httptest.NewRequest(http.MethodGet, "http://relay.test/.well-known/nostr.json?name=reader", nil)119 nip05Result := httptest.NewRecorder()120 tenant.ServeHTTP(nip05Result, nip05)121 if nip05Result.Code != http.StatusUnauthorized {122 t.Fatalf("private NIP-05 exposed unsigned metadata: %d %s", nip05Result.Code, nip05Result.Body.String())123 }124}125126func TestPrivateGitRejectsSignedOutsiderBeforeRepositoryLookup(t *testing.T) {127 _, tenant := testTenant(t)128 p := tenant.Policy()129 p.Features.Grasp = true130 p.Features.Grasp08 = true131 p.Reads = "members"132 if err := tenant.applyPolicy(context.Background(), p); err != nil {133 t.Fatal(err)134 }135 path := "/npub1unknown/private.git/info/refs?service=git-upload-pack"136 outsider := httptest.NewRequest(http.MethodGet, "http://relay.test"+path, nil)137 signRequestWithSecret(t, outsider, "", strings.Repeat("3", 64))138 denied := httptest.NewRecorder()139 tenant.ServeHTTP(denied, outsider)140 if denied.Code != http.StatusUnauthorized || denied.Body.Len() != 0 || denied.Header().Get("WWW-Authenticate") != `Nostr method="GET"` {141 t.Fatalf("signed outsider reached private Git lookup: %d %s", denied.Code, denied.Body.String())142 }143 owner := httptest.NewRequest(http.MethodGet, "http://relay.test"+path, nil)144 signRequest(t, owner, "")145 allowed := httptest.NewRecorder()146 tenant.ServeHTTP(allowed, owner)147 if allowed.Code == http.StatusForbidden || allowed.Code == http.StatusUnauthorized {148 t.Fatalf("owner was denied before repository lookup: %d %s", allowed.Code, allowed.Body.String())149 }150}151152func signRequestWithSecret(t *testing.T, r *http.Request, body, secret string) {153 t.Helper()154 hash := sha256.Sum256([]byte(body))155 tags := [][]string{{"u", r.URL.String()}, {"method", r.Method}, {"payload", hex.EncodeToString(hash[:])}}156 if i := strings.Index(r.URL.Path, ".git/"); i >= 0 {157 tags = [][]string{{"u", r.URL.Scheme + "://" + r.URL.Host + r.URL.Path[:i+4]}, {"method", http.MethodGet}}158 }159 e := event.Event{Kind: 27235, CreatedAt: time.Now().Unix(), Content: "", Tags: tags}160 if err := event.Sign(&e, secret); err != nil {161 t.Fatal(err)162 }163 raw, err := json.Marshal(e)164 if err != nil {165 t.Fatal(err)166 }167 r.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(raw))168}