mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 07:21:32 +02:00
feat(relay): gRPC event-admission (nauthz) service enforcing the write policy
nostr-rs-relay's [grpc] event_admission_server calls Authorization.EventAdmit once per inbound event; we answer PERMIT/DENY so the write policy is enforced at ingest (dynamic, per-event — a static pubkey whitelist can't do inbox/PoW). - proto/nauthz.proto (matches nostr-rs-relay's contract) compiled by build.rs; protoc is vendored (protoc-bin-vendored) so a source build needs no system protobuf-compiler. tonic/prost added (reuse the hyper/tower/h2 stack axum already pulls in); server-only codegen. - infra/relay/nauthz: AdmissionService resolves author + recipient (p-tag) registration via the DB, runs the pure policy engine, returns the decision; served on a loopback socket (spawned in main only when relay enabled + policy != open), mirroring the admin plane. Fail-closed on a malformed request. - db: is_registered_npub(hex) — a no-PII membership check safe to call per event. - tests/relay_admission.rs: L1 handler tests (is_registered_npub membership + outbox/inbox/deny-non-giftwrap/deny-unregistered-recipient through the real event_admit handler against live Postgres).
This commit is contained in:
Generated
+296
-5
@@ -104,6 +104,12 @@ version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.42"
|
||||
@@ -116,6 +122,28 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
|
||||
dependencies = [
|
||||
"async-stream-impl",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream-impl"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
@@ -767,6 +795,12 @@ dependencies = [
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
@@ -1042,13 +1076,38 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
"indexmap",
|
||||
"indexmap 2.14.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http 1.4.2",
|
||||
"indexmap 2.14.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
@@ -1223,7 +1282,7 @@ dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"h2 0.3.27",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"httparse",
|
||||
@@ -1247,6 +1306,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"h2 0.4.15",
|
||||
"http 1.4.2",
|
||||
"http-body 1.0.1",
|
||||
"httparse",
|
||||
@@ -1255,6 +1315,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1271,6 +1332,19 @@ dependencies = [
|
||||
"tokio-rustls 0.24.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-timeout"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
|
||||
dependencies = [
|
||||
"hyper 1.10.1",
|
||||
"hyper-util",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.5.0"
|
||||
@@ -1291,12 +1365,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http 1.4.2",
|
||||
"http-body 1.0.1",
|
||||
"hyper 1.10.1",
|
||||
"libc",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.4",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1442,6 +1521,16 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown 0.12.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
@@ -1478,6 +1567,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -1719,6 +1817,12 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multimap"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.18"
|
||||
@@ -1954,6 +2058,16 @@ version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"indexmap 2.14.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.13"
|
||||
@@ -2081,6 +2195,16 @@ dependencies = [
|
||||
"termtree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
@@ -2114,6 +2238,122 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-build"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"itertools",
|
||||
"log",
|
||||
"multimap",
|
||||
"once_cell",
|
||||
"petgraph",
|
||||
"prettyplease",
|
||||
"prost",
|
||||
"prost-types",
|
||||
"regex",
|
||||
"syn 2.0.118",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-derive"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-types"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
|
||||
dependencies = [
|
||||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa"
|
||||
dependencies = [
|
||||
"protoc-bin-vendored-linux-aarch_64",
|
||||
"protoc-bin-vendored-linux-ppcle_64",
|
||||
"protoc-bin-vendored-linux-s390_64",
|
||||
"protoc-bin-vendored-linux-x86_32",
|
||||
"protoc-bin-vendored-linux-x86_64",
|
||||
"protoc-bin-vendored-macos-aarch_64",
|
||||
"protoc-bin-vendored-macos-x86_64",
|
||||
"protoc-bin-vendored-win32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-linux-aarch_64"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-linux-ppcle_64"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-linux-s390_64"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-linux-x86_32"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-linux-x86_64"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-macos-aarch_64"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-macos-x86_64"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-win32"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3"
|
||||
|
||||
[[package]]
|
||||
name = "quanta"
|
||||
version = "0.12.6"
|
||||
@@ -2247,7 +2487,7 @@ dependencies = [
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"h2 0.3.27",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.32",
|
||||
@@ -2707,6 +2947,8 @@ dependencies = [
|
||||
"k256",
|
||||
"mockall",
|
||||
"monero",
|
||||
"prost",
|
||||
"protoc-bin-vendored",
|
||||
"rand",
|
||||
"reqwest",
|
||||
"rmp-serde",
|
||||
@@ -2720,6 +2962,8 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls 0.25.0",
|
||||
"tokio-test",
|
||||
"tonic",
|
||||
"tonic-build",
|
||||
"tower 0.4.13",
|
||||
"tower-http",
|
||||
"tower_governor",
|
||||
@@ -2828,7 +3072,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"hashlink",
|
||||
"hex",
|
||||
"indexmap",
|
||||
"indexmap 2.14.0",
|
||||
"ipnetwork",
|
||||
"log",
|
||||
"memchr",
|
||||
@@ -3334,6 +3578,50 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"h2 0.4.15",
|
||||
"http 1.4.2",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper 1.10.1",
|
||||
"hyper-timeout",
|
||||
"hyper-util",
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
"prost",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tower 0.4.13",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic-build"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11"
|
||||
dependencies = [
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"prost-build",
|
||||
"prost-types",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.4.13"
|
||||
@@ -3342,8 +3630,11 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"indexmap 1.9.3",
|
||||
"pin-project",
|
||||
"pin-project-lite",
|
||||
"rand",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower-layer",
|
||||
@@ -3612,7 +3903,7 @@ version = "5.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bde15df68e80b16c7d16b9616e80770ad158988daa56a27dccd1e55558b0160"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"indexmap 2.14.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"utoipa-gen",
|
||||
|
||||
+11
@@ -107,6 +107,17 @@ async-trait = "0.1"
|
||||
validator = { version = "0.16", features = ["derive"] }
|
||||
bech32 = "0.12.0"
|
||||
|
||||
# gRPC (nostr-rs-relay event-admission hook, `nauthz`). Runtime-gated by
|
||||
# RELAY_ENABLED; reuses the hyper/tower/h2 stack axum already pulls in.
|
||||
tonic = "0.12"
|
||||
prost = "0.13"
|
||||
|
||||
[build-dependencies]
|
||||
# Compile proto/nauthz.proto. protoc is vendored so building from source needs
|
||||
# no system protobuf-compiler.
|
||||
tonic-build = "0.12"
|
||||
protoc-bin-vendored = "3"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
mockall = "0.12"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
//! Compile the gRPC proto(s). `protoc` is vendored (protoc-bin-vendored) so a
|
||||
//! source build needs no system protobuf-compiler.
|
||||
|
||||
fn main() {
|
||||
// Point tonic-build/prost at the vendored protoc.
|
||||
if let Ok(protoc) = protoc_bin_vendored::protoc_bin_path() {
|
||||
std::env::set_var("PROTOC", protoc);
|
||||
}
|
||||
println!("cargo:rerun-if-changed=proto/nauthz.proto");
|
||||
tonic_build::configure()
|
||||
.build_client(false) // we only serve the admission service, never call it
|
||||
.compile_protos(&["proto/nauthz.proto"], &["proto"])
|
||||
.expect("compile proto/nauthz.proto");
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// nostr-rs-relay event-admission (nauthz) gRPC contract.
|
||||
//
|
||||
// This MUST match the proto nostr-rs-relay ships (its `[grpc]
|
||||
// event_admission_server` calls `Authorization.EventAdmit` once per inbound
|
||||
// event). Keep field numbers/types identical or the relay cannot talk to us.
|
||||
// Upstream: nostr-rs-relay/proto/nauthz.proto.
|
||||
syntax = "proto3";
|
||||
|
||||
package nauthz;
|
||||
|
||||
// Authorization for actions against a relay.
|
||||
service Authorization {
|
||||
// Determine if an event should be admitted to the relay.
|
||||
rpc EventAdmit(EventRequest) returns (EventReply);
|
||||
}
|
||||
|
||||
message Event {
|
||||
bytes id = 1;
|
||||
bytes pubkey = 2;
|
||||
fixed64 created_at = 3;
|
||||
uint64 kind = 4;
|
||||
string content = 5;
|
||||
repeated TagEntry tags = 6;
|
||||
bytes sig = 7;
|
||||
}
|
||||
|
||||
message TagEntry {
|
||||
repeated string values = 1;
|
||||
}
|
||||
|
||||
// Event data + connection metadata for the admission decision.
|
||||
message EventRequest {
|
||||
Event event = 1;
|
||||
optional string ip_addr = 2;
|
||||
optional string origin = 3;
|
||||
optional string user_agent = 4;
|
||||
optional bytes auth_pubkey = 5;
|
||||
optional string nip05 = 6;
|
||||
}
|
||||
|
||||
message EventReply {
|
||||
Decision decision = 1;
|
||||
optional string message = 2;
|
||||
}
|
||||
|
||||
enum Decision {
|
||||
DECISION_UNSPECIFIED = 0;
|
||||
DECISION_PERMIT = 1;
|
||||
DECISION_DENY = 2;
|
||||
}
|
||||
@@ -114,6 +114,20 @@ impl Database {
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Whether an x-only pubkey (canonical lowercase hex) is linked to any user —
|
||||
/// the fast membership check behind the relay write-admission policy. No PII
|
||||
/// returned, so it is safe to call per inbound relay event.
|
||||
#[instrument(skip(self, nostr_pubkey))]
|
||||
pub async fn is_registered_npub(&self, nostr_pubkey: &str) -> Result<bool, AppError> {
|
||||
let exists = sqlx::query_scalar::<_, bool>(
|
||||
"SELECT EXISTS (SELECT 1 FROM users WHERE nostr_pubkey = $1)",
|
||||
)
|
||||
.bind(nostr_pubkey)
|
||||
.fetch_one(self.pool())
|
||||
.await?;
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Replace a user's `pubkey_hash` (derivation-scheme rotation, keyed by the
|
||||
/// unchanged `seed_fingerprint`). Peppered.
|
||||
#[instrument(skip(self, new_pubkey_hash))]
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
//! write policy, enforces admission via `nostr-rs-relay`'s gRPC event-admission
|
||||
//! hook, evaluated by the pure engine in [`policy`].
|
||||
|
||||
pub mod nauthz;
|
||||
mod policy;
|
||||
|
||||
pub use policy::{decide, leading_zero_bits, Admit, EventMeta, WritePolicy, GIFT_WRAP_KIND};
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
//! gRPC event-admission service — the `nostr-rs-relay` `nauthz` hook.
|
||||
//!
|
||||
//! `nostr-rs-relay` (configured with `[grpc] event_admission_server`) calls
|
||||
//! `Authorization.EventAdmit` once per inbound event, before storing it. We
|
||||
//! resolve the author + recipient registration against the DB and run the pure
|
||||
//! [`super::policy`] engine, returning PERMIT/DENY. This is the adapter-specific
|
||||
//! transport for the write policy; a different relay backend could enforce the
|
||||
//! same policy over a different mechanism.
|
||||
//!
|
||||
//! Runs on a loopback socket (like the admin plane); never exposed publicly.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use tonic::{Request, Response, Status};
|
||||
|
||||
use crate::infra::db::Database;
|
||||
|
||||
use super::{decide, Admit, EventMeta, RelayProvider};
|
||||
|
||||
/// Generated from `proto/nauthz.proto` (see `build.rs`).
|
||||
pub mod proto {
|
||||
tonic::include_proto!("nauthz");
|
||||
}
|
||||
|
||||
use proto::authorization_server::{Authorization, AuthorizationServer};
|
||||
use proto::{Decision, EventReply, EventRequest};
|
||||
|
||||
/// The admission decider: the configured relay policy + the user DB.
|
||||
pub struct AdmissionService {
|
||||
relay: Arc<dyn RelayProvider>,
|
||||
db: Database,
|
||||
}
|
||||
|
||||
impl AdmissionService {
|
||||
pub fn new(relay: Arc<dyn RelayProvider>, db: Database) -> Self {
|
||||
Self { relay, db }
|
||||
}
|
||||
}
|
||||
|
||||
fn permit() -> EventReply {
|
||||
EventReply {
|
||||
decision: Decision::Permit as i32,
|
||||
message: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn deny(msg: &str) -> EventReply {
|
||||
EventReply {
|
||||
decision: Decision::Deny as i32,
|
||||
message: Some(msg.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl Authorization for AdmissionService {
|
||||
async fn event_admit(
|
||||
&self,
|
||||
request: Request<EventRequest>,
|
||||
) -> Result<Response<EventReply>, Status> {
|
||||
let req = request.into_inner();
|
||||
let Some(event) = req.event else {
|
||||
// Fail closed on a malformed request.
|
||||
return Ok(Response::new(deny("missing event")));
|
||||
};
|
||||
|
||||
// Canonical lowercase hex (matches how we store/verify npubs).
|
||||
let author_hex = hex::encode(&event.pubkey);
|
||||
let id_hex = hex::encode(&event.id);
|
||||
|
||||
// Author registration.
|
||||
let author_registered = self
|
||||
.db
|
||||
.is_registered_npub(&author_hex)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
|
||||
// Recipient registration: any `p` tag that is a registered npub. Only
|
||||
// resolved when needed (skipped once the author is already registered).
|
||||
let mut recipient_registered = false;
|
||||
if !author_registered {
|
||||
for tag in &event.tags {
|
||||
if tag.values.len() >= 2 && tag.values[0] == "p" {
|
||||
let p = tag.values[1].to_lowercase();
|
||||
if self.db.is_registered_npub(&p).await.unwrap_or(false) {
|
||||
recipient_registered = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let meta = EventMeta {
|
||||
author_pubkey: &author_hex,
|
||||
kind: event.kind,
|
||||
id_hex: &id_hex,
|
||||
};
|
||||
let reply = match decide(
|
||||
&meta,
|
||||
self.relay.write_policy(),
|
||||
self.relay.inbound_pow_bits(),
|
||||
author_registered,
|
||||
recipient_registered,
|
||||
) {
|
||||
Admit::Permit => permit(),
|
||||
Admit::Deny(msg) => deny(msg),
|
||||
};
|
||||
Ok(Response::new(reply))
|
||||
}
|
||||
}
|
||||
|
||||
/// Serve the admission service on `addr` (a loopback socket). Returns when the
|
||||
/// server stops or errors.
|
||||
pub async fn serve(
|
||||
addr: std::net::SocketAddr,
|
||||
relay: Arc<dyn RelayProvider>,
|
||||
db: Database,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let svc = AdmissionService { relay, db };
|
||||
tonic::transport::Server::builder()
|
||||
.add_service(AuthorizationServer::new(svc))
|
||||
.serve(addr)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
+25
@@ -231,6 +231,31 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
});
|
||||
}
|
||||
|
||||
// Nostr relay event-admission (nauthz) gRPC service — only for a non-`open`
|
||||
// write policy (open needs no admission). Loopback socket, like the admin
|
||||
// plane; nostr-rs-relay is configured to call it per inbound event.
|
||||
if let Some(relay) = state.relay.clone() {
|
||||
use smirk_backend_core::infra::relay::WritePolicy;
|
||||
if relay.write_policy() != WritePolicy::Open {
|
||||
let addr: std::net::SocketAddr = state
|
||||
.config
|
||||
.messaging
|
||||
.relay
|
||||
.admission_bind
|
||||
.parse()
|
||||
.map_err(|e| format!("invalid RELAY_ADMISSION_BIND: {e}"))?;
|
||||
let db = state.db.clone();
|
||||
tracing::info!("relay event-admission (nauthz) listening on {addr}");
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) =
|
||||
smirk_backend_core::infra::relay::nauthz::serve(addr, relay, db).await
|
||||
{
|
||||
tracing::error!(error = %e, "relay admission service exited");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let app = build_router(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
//! L1 integration: the relay event-admission (nauthz) gRPC handler.
|
||||
//!
|
||||
//! Drives `AdmissionService::event_admit` directly (no gRPC transport — that is
|
||||
//! covered by the docker-relay E2E) to prove the handler wiring: p-tag
|
||||
//! extraction, the DB registration lookup, and the policy decision, end to end
|
||||
//! against live Postgres. The pure policy branches are covered in unit tests.
|
||||
|
||||
mod common;
|
||||
|
||||
use smirk_backend_core::infra::relay::nauthz::proto::authorization_server::Authorization;
|
||||
use smirk_backend_core::infra::relay::nauthz::{proto, AdmissionService};
|
||||
use smirk_backend_core::infra::relay::{self, RelayProvider};
|
||||
use smirk_backend_core::models::db::NewUser;
|
||||
use std::sync::Arc;
|
||||
use tonic::Request;
|
||||
|
||||
/// A 64-char lowercase-hex pubkey/id (valid for hex::decode).
|
||||
fn hex64(seed: &str) -> String {
|
||||
let base = format!("{seed}{}", "0".repeat(64));
|
||||
base[..64].to_string()
|
||||
}
|
||||
|
||||
async fn register_npub(db: &smirk_backend_core::infra::db::Database, npub_hex: &str) {
|
||||
db.create_user(NewUser {
|
||||
username: None,
|
||||
pubkey_hash: Some(format!("pk-{}", uuid::Uuid::new_v4())),
|
||||
nostr_pubkey: Some(npub_hex.to_string()),
|
||||
wallet_birthday: None,
|
||||
seed_fingerprint: None,
|
||||
xmr_start_height: None,
|
||||
wow_start_height: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn inbox_outbox_relay(base: &smirk_backend_core::config::Config) -> Arc<dyn RelayProvider> {
|
||||
let mut cfg = base.clone();
|
||||
cfg.messaging.relay.enabled = true;
|
||||
cfg.messaging.relay.write_policy = "inbox-outbox".into();
|
||||
cfg.messaging.relay.advertised_url = "wss://relay.example.org".into();
|
||||
cfg.messaging.relay.inbound_pow_bits = 0;
|
||||
relay::from_config(&cfg).unwrap().unwrap()
|
||||
}
|
||||
|
||||
fn event(author_hex: &str, kind: u64, p_tags: &[&str], id_hex: &str) -> proto::EventRequest {
|
||||
proto::EventRequest {
|
||||
event: Some(proto::Event {
|
||||
id: hex::decode(id_hex).unwrap(),
|
||||
pubkey: hex::decode(author_hex).unwrap(),
|
||||
created_at: 0,
|
||||
kind,
|
||||
content: String::new(),
|
||||
tags: p_tags
|
||||
.iter()
|
||||
.map(|p| proto::TagEntry {
|
||||
values: vec!["p".to_string(), (*p).to_string()],
|
||||
})
|
||||
.collect(),
|
||||
sig: vec![],
|
||||
}),
|
||||
ip_addr: None,
|
||||
origin: None,
|
||||
user_agent: None,
|
||||
auth_pubkey: None,
|
||||
nip05: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn admit(svc: &AdmissionService, req: proto::EventRequest) -> bool {
|
||||
let reply = svc
|
||||
.event_admit(Request::new(req))
|
||||
.await
|
||||
.unwrap()
|
||||
.into_inner();
|
||||
reply.decision == proto::Decision::Permit as i32
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn is_registered_npub_membership() {
|
||||
let app = require_app!();
|
||||
let npub = hex64("aaaa");
|
||||
assert!(!app.state.db.is_registered_npub(&npub).await.unwrap());
|
||||
register_npub(&app.state.db, &npub).await;
|
||||
assert!(app.state.db.is_registered_npub(&npub).await.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admission_inbox_outbox_end_to_end() {
|
||||
let app = require_app!();
|
||||
let svc = AdmissionService::new(inbox_outbox_relay(&app.state.config), app.state.db.clone());
|
||||
|
||||
let author = hex64("b1"); // registered author (outbox)
|
||||
let recipient = hex64("b2"); // registered recipient (inbox)
|
||||
let stranger = hex64("b3"); // unregistered
|
||||
register_npub(&app.state.db, &author).await;
|
||||
register_npub(&app.state.db, &recipient).await;
|
||||
let ff = "ff".repeat(32);
|
||||
|
||||
// Outbox: a registered author's own event → PERMIT.
|
||||
assert!(
|
||||
admit(&svc, event(&author, 1, &[], &ff)).await,
|
||||
"registered author outbox"
|
||||
);
|
||||
|
||||
// Inbox: an unregistered author's gift-wrap (1059) to a registered recipient → PERMIT.
|
||||
assert!(
|
||||
admit(&svc, event(&stranger, 1059, &[&recipient], &ff)).await,
|
||||
"gift-wrap to registered recipient"
|
||||
);
|
||||
|
||||
// External non-gift-wrap → DENY.
|
||||
assert!(
|
||||
!admit(&svc, event(&stranger, 1, &[&recipient], &ff)).await,
|
||||
"external non-gift-wrap rejected"
|
||||
);
|
||||
|
||||
// Gift-wrap to an UNREGISTERED recipient → DENY.
|
||||
assert!(
|
||||
!admit(&svc, event(&stranger, 1059, &[&hex64("b9")], &ff)).await,
|
||||
"gift-wrap to unregistered recipient rejected"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user