From c2c3ebc2e1bc0541040ba3f30f877a9bd11a9d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sun, 9 Jun 2024 22:49:44 +0200 Subject: [PATCH 1/8] Add strfry policies and members-only LDAP policy This will look up nostr pubkeys in the LDAP directory to allow or deny publishing notes to the relay. --- docker-compose.yml | 9 ++++++- docker/strfry/strfry.conf | 4 +-- extras/strfry/ldap-policy.ts | 46 ++++++++++++++++++++++++++++++++++ extras/strfry/strfry-policy.ts | 23 +++++++++++++++++ 4 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 extras/strfry/ldap-policy.ts create mode 100755 extras/strfry/strfry-policy.ts diff --git a/docker-compose.yml b/docker-compose.yml index ec7069f..165f4cf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -108,15 +108,22 @@ services: - redis nostr-relay: - image: pluja/strfry:latest + image: gitea.kosmos.org/kosmos/strfry-deno:1.0.0 volumes: - ./docker/strfry/strfry.conf:/etc/strfry.conf + - ./extras/strfry/ldap-policy.ts:/opt/ldap-policy.ts + - ./extras/strfry/strfry-policy.ts:/opt/strfry-policy.ts - strfry-data:/app/strfry-db networks: - external_network - internal_network ports: - "4777:7777" + environment: + LDAP_URL: 'ldap://ldap:3389' + LDAP_BIND_DN: 'cn=Directory Manager' + LDAP_PASSWORD: passthebutter + LDAP_SEARCH_DN: 'ou=kosmos.org,cn=users,dc=kosmos,dc=org' # phpldapadmin: # image: osixia/phpldapadmin:0.9.0 diff --git a/docker/strfry/strfry.conf b/docker/strfry/strfry.conf index 9342387..b142ca7 100644 --- a/docker/strfry/strfry.conf +++ b/docker/strfry/strfry.conf @@ -54,7 +54,7 @@ relay { info { # NIP-11: Name of this server. Short/descriptive (< 30 characters) - name = "akkounts-nostr-relay" + name = "Akkounts Nostr Relay" # NIP-11: Detailed information about relay, free-form description = "Local strfry instance for akkounts development" @@ -86,7 +86,7 @@ relay { writePolicy { # If non-empty, path to an executable script that implements the writePolicy plugin logic - plugin = "" + plugin = "/opt/strfry-policy.ts" } compression { diff --git a/extras/strfry/ldap-policy.ts b/extras/strfry/ldap-policy.ts new file mode 100644 index 0000000..6ac8fda --- /dev/null +++ b/extras/strfry/ldap-policy.ts @@ -0,0 +1,46 @@ +import type { Policy } from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/mod.ts'; +import { Client } from 'npm:ldapts'; +import { load } from "https://deno.land/std@0.224.0/dotenv/mod.ts"; + +const env = await load({ export: true }); +const url = Deno.env.get("LDAP_URL"); +const bindDN = Deno.env.get("LDAP_BIND_DN"); +const password = Deno.env.get("LDAP_PASSWORD"); +const searchDN = Deno.env.get("LDAP_SEARCH_DN"); + +const ldapPolicy: Policy = async (msg) => { + const client = new Client({ url }); + const { pubkey, kind, tags } = msg.event; + let out = { id: msg.event.id } + + try { + await client.bind(bindDN, password); + + const { searchEntries } = await client.search(searchDN, { + filter: `(nostrKey=${pubkey})`, + attributes: ['nostrKey'] + }); + + const memberKey = searchEntries[0]?.nostrKey; + + const accepted = (memberKey === pubkey); + // TODO if kind is 9735, check that "description" tag contains valid 9734 event, + // signed by memberKey and with "p" tag being the same as pubkey (receipt sender) + + if (accepted) { + out['action'] = 'accept'; + out['msg'] = ''; + } else { + out['action'] = 'reject'; + out['msg'] = 'Only members can publish notes on this relay'; + } + } catch (ex) { + out['action'] = 'reject'; + out['msg'] = 'Auth service temporarily unavailable'; + } finally { + await client.unbind(); + return out; + } +}; + +export default ldapPolicy; diff --git a/extras/strfry/strfry-policy.ts b/extras/strfry/strfry-policy.ts new file mode 100755 index 0000000..0838170 --- /dev/null +++ b/extras/strfry/strfry-policy.ts @@ -0,0 +1,23 @@ +#!/bin/sh +//bin/true; exec deno run -A "$0" "$@" +import { + antiDuplicationPolicy, + hellthreadPolicy, + pipeline, + rateLimitPolicy, + readStdin, + writeStdout, +} from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/mod.ts'; + +import ldapPolicy from './ldap-policy.ts'; + +for await (const msg of readStdin()) { + const result = await pipeline(msg, [ + [hellthreadPolicy, { limit: 10 }], + [antiDuplicationPolicy, { ttl: 60000, minLength: 50 }], + [rateLimitPolicy, { whitelist: ['127.0.0.1'] }], + [ldapPolicy], + ]); + + writeStdout(result); +} From 2a675fd135a5edce5a7f71ec0e9635beb6644d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sun, 9 Jun 2024 23:15:56 +0200 Subject: [PATCH 2/8] Hand LDAP config to policy from main policy file Deployments will differ in production. The policy itself just needs the configs, but should not care where credentials are fetched from. --- extras/strfry/ldap-policy.ts | 20 ++++++++++---------- extras/strfry/strfry-policy.ts | 14 ++++++++++++-- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/extras/strfry/ldap-policy.ts b/extras/strfry/ldap-policy.ts index 6ac8fda..a200639 100644 --- a/extras/strfry/ldap-policy.ts +++ b/extras/strfry/ldap-policy.ts @@ -1,22 +1,22 @@ import type { Policy } from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/mod.ts'; import { Client } from 'npm:ldapts'; -import { load } from "https://deno.land/std@0.224.0/dotenv/mod.ts"; -const env = await load({ export: true }); -const url = Deno.env.get("LDAP_URL"); -const bindDN = Deno.env.get("LDAP_BIND_DN"); -const password = Deno.env.get("LDAP_PASSWORD"); -const searchDN = Deno.env.get("LDAP_SEARCH_DN"); +interface LdapConfig { + url: string; + bindDN: string; + password: string; + searchDN: string; +} -const ldapPolicy: Policy = async (msg) => { - const client = new Client({ url }); +const ldapPolicy: Policy = async (msg, opts) => { + const client = new Client({ url: opts.url }); const { pubkey, kind, tags } = msg.event; let out = { id: msg.event.id } try { - await client.bind(bindDN, password); + await client.bind(opts.bindDN, opts.password); - const { searchEntries } = await client.search(searchDN, { + const { searchEntries } = await client.search(opts.searchDN, { filter: `(nostrKey=${pubkey})`, attributes: ['nostrKey'] }); diff --git a/extras/strfry/strfry-policy.ts b/extras/strfry/strfry-policy.ts index 0838170..15fafe9 100755 --- a/extras/strfry/strfry-policy.ts +++ b/extras/strfry/strfry-policy.ts @@ -8,15 +8,25 @@ import { readStdin, writeStdout, } from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/mod.ts'; - import ldapPolicy from './ldap-policy.ts'; +import { load } from "https://deno.land/std@0.224.0/dotenv/mod.ts"; + +const dirname = new URL('.', import.meta.url).pathname; +await load({ envPath: `${dirname}/.env`, export: true }); + +const ldapConfig = { + url: Deno.env.get("LDAP_URL"), + bindDN: Deno.env.get("LDAP_BIND_DN"), + password: Deno.env.get("LDAP_PASSWORD"), + searchDN: Deno.env.get("LDAP_SEARCH_DN"), +} for await (const msg of readStdin()) { const result = await pipeline(msg, [ [hellthreadPolicy, { limit: 10 }], [antiDuplicationPolicy, { ttl: 60000, minLength: 50 }], [rateLimitPolicy, { whitelist: ['127.0.0.1'] }], - [ldapPolicy], + [ldapPolicy, ldapConfig], ]); writeStdout(result); From 62fe0d8fac2fda965677b0c41152b70345aa9b51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Tue, 11 Jun 2024 22:05:07 +0200 Subject: [PATCH 3/8] Add nostrKey to default org service ACI --- app/services/ldap_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/ldap_service.rb b/app/services/ldap_service.rb index ab7dd94..8364eb5 100644 --- a/app/services/ldap_service.rb +++ b/app/services/ldap_service.rb @@ -101,7 +101,7 @@ class LdapService < ApplicationService dn = "ou=#{ou},cn=users,#{ldap_suffix}" aci = <<-EOS -(target="ldap:///cn=*,ou=#{ou},cn=users,#{ldap_suffix}")(targetattr="cn || sn || uid || mail || userPassword || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{ldap_suffix}";) +(target="ldap:///cn=*,ou=#{ou},cn=users,#{ldap_suffix}")(targetattr="cn || sn || uid || userPassword || mail || mailRoutingAddress || serviceEnabled || nostrKey || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{ldap_suffix}";) EOS attrs = { From 433ac4dc8ee51fff57a167e33b538320b5f431b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Tue, 11 Jun 2024 22:06:12 +0200 Subject: [PATCH 4/8] Use new strfry Docker image --- docker-compose.yml | 6 +++--- docker/strfry/strfry.conf | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 165f4cf..4b70859 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -107,13 +107,13 @@ services: - minio - redis - nostr-relay: - image: gitea.kosmos.org/kosmos/strfry-deno:1.0.0 + strfry: + image: gitea.kosmos.org/kosmos/strfry-deno:1.1.1 volumes: - ./docker/strfry/strfry.conf:/etc/strfry.conf - ./extras/strfry/ldap-policy.ts:/opt/ldap-policy.ts - ./extras/strfry/strfry-policy.ts:/opt/strfry-policy.ts - - strfry-data:/app/strfry-db + - strfry-data:/var/lib/strfry networks: - external_network - internal_network diff --git a/docker/strfry/strfry.conf b/docker/strfry/strfry.conf index b142ca7..2a2bd09 100644 --- a/docker/strfry/strfry.conf +++ b/docker/strfry/strfry.conf @@ -3,7 +3,7 @@ ## # Directory that contains the strfry LMDB database (restart required) -db = "./strfry-db/" +db = "/var/lib/strfry/" dbParams { # Maximum number of threads/processes that can simultaneously have LMDB transactions open (restart required) From 61cb8f4941c84f165ce6e793a12975693979c02e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Tue, 11 Jun 2024 22:06:51 +0200 Subject: [PATCH 5/8] Add script for syncing notes from remote relays --- docker-compose.yml | 1 + extras/strfry/strfry-sync.ts | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 extras/strfry/strfry-sync.ts diff --git a/docker-compose.yml b/docker-compose.yml index 4b70859..d38ed17 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -113,6 +113,7 @@ services: - ./docker/strfry/strfry.conf:/etc/strfry.conf - ./extras/strfry/ldap-policy.ts:/opt/ldap-policy.ts - ./extras/strfry/strfry-policy.ts:/opt/strfry-policy.ts + - ./extras/strfry/strfry-sync.ts:/opt/strfry-sync.ts - strfry-data:/var/lib/strfry networks: - external_network diff --git a/extras/strfry/strfry-sync.ts b/extras/strfry/strfry-sync.ts new file mode 100644 index 0000000..4b24540 --- /dev/null +++ b/extras/strfry/strfry-sync.ts @@ -0,0 +1,39 @@ +import { load } from "https://deno.land/std@0.224.0/dotenv/mod.ts"; +import { Client } from 'npm:ldapts'; + +const dirname = new URL('.', import.meta.url).pathname; +await load({ envPath: `${dirname}/.env`, export: true }); + +const opts = { + url: Deno.env.get("LDAP_URL"), + bindDN: Deno.env.get("LDAP_BIND_DN"), + password: Deno.env.get("LDAP_PASSWORD"), + searchDN: Deno.env.get("LDAP_SEARCH_DN"), + relayUrl: Deno.args[0] +} + +const client = new Client({ url: opts.url }); + +try { + await client.bind(opts.bindDN, opts.password); + + const { searchEntries } = await client.search(opts.searchDN, { + filter: `(nostrKey=*)`, + attributes: ['nostrKey'] + }); + + const pubkeys = searchEntries.map(e => e.nostrKey); + const filter = JSON.stringify({ authors: pubkeys }); + + const p = Deno.run({ cmd: [ + "strfry", "sync", opts.relayUrl, + "--dir", "down", "--filter", filter + ]}); + + const result = await p.status(); + + Deno.exit(result.code); +} catch (ex) { + console.error(ex); + Deno.exit(1); +} From 0e472bc3118d5f28e3a681b916578956e7505392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 19 Jun 2024 15:43:24 +0200 Subject: [PATCH 6/8] Improve strfry extras usage --- docker-compose.yml | 4 +--- docker/strfry/strfry.conf | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d38ed17..32410ff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -111,9 +111,7 @@ services: image: gitea.kosmos.org/kosmos/strfry-deno:1.1.1 volumes: - ./docker/strfry/strfry.conf:/etc/strfry.conf - - ./extras/strfry/ldap-policy.ts:/opt/ldap-policy.ts - - ./extras/strfry/strfry-policy.ts:/opt/strfry-policy.ts - - ./extras/strfry/strfry-sync.ts:/opt/strfry-sync.ts + - ./extras/strfry:/opt/strfry - strfry-data:/var/lib/strfry networks: - external_network diff --git a/docker/strfry/strfry.conf b/docker/strfry/strfry.conf index 2a2bd09..67bd53e 100644 --- a/docker/strfry/strfry.conf +++ b/docker/strfry/strfry.conf @@ -86,7 +86,7 @@ relay { writePolicy { # If non-empty, path to an executable script that implements the writePolicy plugin logic - plugin = "/opt/strfry-policy.ts" + plugin = "/opt/strfry/strfry-policy.ts" } compression { From 0daac33915f827494feb4a5d6bb9ba5d49e6b904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 19 Jun 2024 15:43:56 +0200 Subject: [PATCH 7/8] Allow non-members to publish zap receipts for members --- extras/strfry/deno.json | 5 +++++ extras/strfry/ldap-policy.ts | 35 ++++++++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 extras/strfry/deno.json diff --git a/extras/strfry/deno.json b/extras/strfry/deno.json new file mode 100644 index 0000000..1630812 --- /dev/null +++ b/extras/strfry/deno.json @@ -0,0 +1,5 @@ +{ + "imports": { + "@nostr/tools": "jsr:@nostr/tools@^2.3.1" + } +} diff --git a/extras/strfry/ldap-policy.ts b/extras/strfry/ldap-policy.ts index a200639..1798ef6 100644 --- a/extras/strfry/ldap-policy.ts +++ b/extras/strfry/ldap-policy.ts @@ -1,5 +1,6 @@ import type { Policy } from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/mod.ts'; import { Client } from 'npm:ldapts'; +import { nip57 } from '@nostr/tools'; interface LdapConfig { url: string; @@ -10,9 +11,34 @@ interface LdapConfig { const ldapPolicy: Policy = async (msg, opts) => { const client = new Client({ url: opts.url }); - const { pubkey, kind, tags } = msg.event; + const { kind, tags } = msg.event; + let { pubkey } = msg.event; let out = { id: msg.event.id } + // Zap receipt + if (kind === 9735) { + let invalidRequest = false; + const descriptionTag = tags.find(([t, v]) => t === 'description' && v); + const invalidZapRequestMsg = 'Zap receipts must contain a valid zap request from a relay member'; + + if (typeof descriptionTag === 'undefined') { + out['action'] = 'reject'; + out['msg'] = invalidZapRequestMsg; + return out; + } + + const zapRequestJSON = descriptionTag[1]; + const validationResult = nip57.validateZapRequest(zapRequestJSON); + + if (validationResult === null) { + pubkey = JSON.parse(zapRequestJSON).pubkey; + } else { + out['action'] = 'reject'; + out['msg'] = invalidZapRequestMsg; + return out; + } + } + try { await client.bind(opts.bindDN, opts.password); @@ -20,14 +46,9 @@ const ldapPolicy: Policy = async (msg, opts) => { filter: `(nostrKey=${pubkey})`, attributes: ['nostrKey'] }); - const memberKey = searchEntries[0]?.nostrKey; - const accepted = (memberKey === pubkey); - // TODO if kind is 9735, check that "description" tag contains valid 9734 event, - // signed by memberKey and with "p" tag being the same as pubkey (receipt sender) - - if (accepted) { + if (memberKey === pubkey) { out['action'] = 'accept'; out['msg'] = ''; } else { From 00b73b06d7d284e338a1f3076cbdac6b159fdf12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 19 Jun 2024 15:56:45 +0200 Subject: [PATCH 8/8] Remove obsolete variable --- extras/strfry/ldap-policy.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/extras/strfry/ldap-policy.ts b/extras/strfry/ldap-policy.ts index 1798ef6..423c74e 100644 --- a/extras/strfry/ldap-policy.ts +++ b/extras/strfry/ldap-policy.ts @@ -17,7 +17,6 @@ const ldapPolicy: Policy = async (msg, opts) => { // Zap receipt if (kind === 9735) { - let invalidRequest = false; const descriptionTag = tags.find(([t, v]) => t === 'description' && v); const invalidZapRequestMsg = 'Zap receipts must contain a valid zap request from a relay member';