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 = { diff --git a/docker-compose.yml b/docker-compose.yml index ec7069f..32410ff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -107,16 +107,22 @@ services: - minio - redis - nostr-relay: - image: pluja/strfry:latest + strfry: + image: gitea.kosmos.org/kosmos/strfry-deno:1.1.1 volumes: - ./docker/strfry/strfry.conf:/etc/strfry.conf - - strfry-data:/app/strfry-db + - ./extras/strfry:/opt/strfry + - strfry-data:/var/lib/strfry 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..67bd53e 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) @@ -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/strfry-policy.ts" } compression { 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 new file mode 100644 index 0000000..423c74e --- /dev/null +++ b/extras/strfry/ldap-policy.ts @@ -0,0 +1,66 @@ +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; + bindDN: string; + password: string; + searchDN: string; +} + +const ldapPolicy: Policy = async (msg, opts) => { + const client = new Client({ url: opts.url }); + const { kind, tags } = msg.event; + let { pubkey } = msg.event; + let out = { id: msg.event.id } + + // Zap receipt + if (kind === 9735) { + 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); + + const { searchEntries } = await client.search(opts.searchDN, { + filter: `(nostrKey=${pubkey})`, + attributes: ['nostrKey'] + }); + const memberKey = searchEntries[0]?.nostrKey; + + if (memberKey === pubkey) { + 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..15fafe9 --- /dev/null +++ b/extras/strfry/strfry-policy.ts @@ -0,0 +1,33 @@ +#!/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'; +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, ldapConfig], + ]); + + writeStdout(result); +} 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); +}