Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proper Role Checking in Authorization #16

Merged
merged 2 commits into from
Jun 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 23 additions & 25 deletions main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,7 @@ import {
} from "./resolvers/event.ts";
import Landing from "./routes/landing.tsx";
import Error404 from "./routes/_404.tsx";
import {
informationPubkeyStringify,
RelayInformation,
RelayInformationStore,
RelayInformationStringify,
} from "./resolvers/nip11.ts";
import { RelayInfomationBase, RelayInformation, RelayInformationStore } from "./resolvers/nip11.ts";
import { func_GetEventsByFilter, func_WriteRegularEvent } from "./resolvers/event.ts";
import { Cookie, getCookies, setCookie } from "https://deno.land/[email protected]/http/cookie.ts";
import { Event_V2, Kind_V2 } from "./events.ts";
Expand Down Expand Up @@ -89,8 +84,9 @@ const ENV_relayed_pubkey = "relayed_pubkey";
export async function run(args: {
port?: number;
admin?: PublicKey;
auth_required: boolean;
default_policy: DefaultPolicy;
default_information?: RelayInformationStringify;
default_information?: RelayInfomationBase;
kv?: Deno.Kv;
_debug?: boolean;
}): Promise<Error | Relay> {
Expand All @@ -114,22 +110,19 @@ export async function run(args: {
const get_events_by_filter = get_events_by_filter_sqlite(db);

// Administrator Keys
let admin_pubkey: string | undefined | PublicKey | Error = args.default_information?.pubkey;
if (admin_pubkey == undefined) {
if (args.admin == undefined) {
const env_pubkey = Deno.env.get(ENV_relayed_pubkey);
if (env_pubkey == undefined) {
return new Error(
"public key is not set. Please set env var $relayed_pubkey or pass default_information.pubkey in the argument",
);
}
admin_pubkey = env_pubkey;
}

admin_pubkey = PublicKey.FromString(admin_pubkey);
if (admin_pubkey instanceof Error) {
return admin_pubkey;
const p = PublicKey.FromString(env_pubkey);
if (p instanceof Error) {
return p;
}
args.admin = p;
}

// Relay Key
// let system_key: string | PrivateKey | Error = args.system_key;
// if (typeof system_key == "string") {
Expand Down Expand Up @@ -159,8 +152,7 @@ export async function run(args: {
kv,
{
...args.default_information,
auth_required: args.default_information?.auth_required || false,
pubkey: admin_pubkey,
pubkey: args.admin,
},
);

Expand All @@ -177,21 +169,23 @@ export async function run(args: {
},
root_handler({
...args,
is_member: async (pubkey: string) => {
is_member: ((admin: PublicKey) => async (pubkey: string) => {
const key = PublicKey.FromString(pubkey);
if (key instanceof Error) {
return key;
}
// admin is always a member
if (key.hex == admin_pubkey.hex) {
if (key.hex == admin.hex) {
return true;
}
const policy = await policyStore.resolvePolicyByKind(NostrKind.TEXT_NOTE);
if (policy instanceof Error) {
return policy;
}
return policy.allow.has(pubkey) && !policy.block.has(pubkey);
},
const policyAllow = policy.allow.has(pubkey);
const policyBlock = policy.block.has(pubkey);
return policyAllow && !policyBlock;
})(args.admin),
// deletion
delete_event: delete_event_sqlite(db),
delete_events_from_pubkey: async () => {
Expand Down Expand Up @@ -220,6 +214,10 @@ export async function run(args: {

const shutdown = async () => {
await server.shutdown();
for (const socket of connections.keys()) {
socket.close();
}
console.log("close db");
kv.close();
db?.close();
};
Expand Down Expand Up @@ -275,6 +273,8 @@ const root_handler = (
write_replaceable_event: func_WriteReplaceableEvent;
// relay
get_relay_members: func_GetRelayMembers;
// config
auth_required: boolean;
kv: Deno.Kv;
_debug: boolean;
},
Expand Down Expand Up @@ -327,7 +327,6 @@ async (req: Request, info: Deno.ServeHandlerInfo) => {
}
return ws_handler({
...args,
auth_required: relay_info.auth_required,
})(req, info);
}
}
Expand Down Expand Up @@ -457,8 +456,7 @@ const information_handler = async (args: { relayInformationStore: RelayInformati
if (storeInformation instanceof Error) {
return new Response(render(Error404()), { status: 404 });
}
const information = informationPubkeyStringify(storeInformation);
const resp = new Response(JSON.stringify(information), {
const resp = new Response(JSON.stringify(storeInformation), {
status: 200,
});
resp.headers.set("content-type", "application/json; charset=utf-8");
Expand Down
9 changes: 7 additions & 2 deletions resolvers/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type interface_GetEventsByAuthors = {
get_events_by_authors: func_GetEventsByAuthors;
};

export type func_GetEventsByFilter = (filter: NostrFilter) => Promise<NostrEvent[]>;
export type func_GetEventsByFilter = (filter: NostrFilter) => Promise<NostrEvent[] | SqliteError>;

export type func_WriteRegularEvent = (event: NostrEvent) => Promise<boolean | Error>;

Expand Down Expand Up @@ -84,7 +84,12 @@ export const get_events_by_filter_sqlite =
params.push(filter.limit || 200);

console.log(sql, "\n", params, "\n", filter);
const results = db.query<[string]>(sql, params);
let results: [string][];
try {
results = db.query<[string]>(sql, params);
} catch (e) {
return e as SqliteError;
}
return results.map((r) => JSON.parse(r[0]) as NostrEvent);
};

Expand Down
15 changes: 1 addition & 14 deletions resolvers/nip11.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import { software, supported_nips } from "../main.tsx";
import { PublicKey } from "../nostr.ts/key.ts";

type RelayInfomationBase = {
export type RelayInfomationBase = {
name?: string;
description?: string;
contact?: string;
icon?: string;
auth_required: boolean;
};

export type RelayInformationStringify = {
pubkey?: string;
} & RelayInfomationBase;

export type RelayInformationParsed = {
pubkey?: PublicKey;
} & RelayInfomationBase;
Expand All @@ -26,7 +21,6 @@ export type RelayInformation = {
software?: string;
version?: string;
icon?: string;
auth_required: boolean;
};

export class RelayInformationStore {
Expand Down Expand Up @@ -62,10 +56,3 @@ export class RelayInformationStore {
return { ...this.default_information, ...new_information, software, supported_nips };
};
}

export function informationPubkeyStringify(info: RelayInformationParsed): RelayInformationStringify {
if (!info.pubkey) {
return info as RelayInfomationBase;
}
return { ...info, pubkey: info.pubkey.hex };
}
88 changes: 70 additions & 18 deletions test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
// deno-lint-ignore-file no-empty
import { Relay, run, software, supported_nips } from "./main.tsx";
import { assertEquals } from "https://deno.land/[email protected]/assert/assert_equals.ts";
import { assertIsError } from "https://deno.land/[email protected]/assert/mod.ts";
import { assertIsError, assertNotInstanceOf } from "https://deno.land/[email protected]/assert/mod.ts";
import { fail } from "https://deno.land/[email protected]/assert/fail.ts";

import * as client_test from "./nostr.ts/relay-single-test.ts";
import { ChannelCreation, ChannelEdition } from "./events.ts";
import { Kind_V2 } from "./events.ts";
import { InMemoryAccountContext, NostrKind, RelayResponse_Event, sign_event_v2 } from "./nostr.ts/nostr.ts";
import {
InMemoryAccountContext,
NostrKind,
RelayResponse_Event,
sign_event_v2,
Signer,
} from "./nostr.ts/nostr.ts";
import { prepareNormalNostrEvent } from "./nostr.ts/event.ts";
import { RelayRejectedEvent, SingleRelayConnection, SubscriptionStream } from "./nostr.ts/relay-single.ts";
import { PrivateKey } from "./nostr.ts/key.ts";
import { sleep } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts";

const test_kv = async () => {
try {
Expand All @@ -33,10 +40,8 @@ Deno.test({
// ignore: true,
fn: async (t) => {
const relay = await run({
default_information: {
pubkey: test_ctx.publicKey.hex,
auth_required: false,
},
auth_required: false,
admin: test_ctx.publicKey,
default_policy: {
allowed_kinds: [NostrKind.Long_Form, NostrKind.Encrypted_Custom_App_Data],
},
Expand Down Expand Up @@ -162,10 +167,8 @@ Deno.test({
// ignore: true,
fn: async () => {
await using relay = await run({
default_information: {
pubkey: test_ctx.publicKey.hex,
auth_required: false,
},
auth_required: false,
admin: test_ctx.publicKey,
default_policy: {
allowed_kinds: "none",
},
Expand Down Expand Up @@ -209,10 +212,8 @@ Deno.test({
// ignore: true,
fn: async () => {
const relay = await run({
default_information: {
pubkey: test_ctx.publicKey.hex,
auth_required: false,
},
auth_required: false,
admin: test_ctx.publicKey,
default_policy: {
allowed_kinds: "none",
},
Expand Down Expand Up @@ -286,9 +287,9 @@ Deno.test({
},
default_information: {
name: "Nostr Relay",
pubkey: test_ctx.publicKey.hex,
auth_required: false,
},
auth_required: false,
admin: test_ctx.publicKey,
kv: await test_kv(),
// system_key: PrivateKey.Generate(),
}) as Relay;
Expand All @@ -300,7 +301,6 @@ Deno.test({
pubkey: test_ctx.publicKey,
software,
supported_nips,
auth_required: false,
});
});

Expand All @@ -318,7 +318,6 @@ Deno.test({
},
software,
supported_nips,
auth_required: false,
});
});

Expand Down Expand Up @@ -361,6 +360,59 @@ Deno.test({
},
});

Deno.test({
name: "Authorization",
// ignore: true,
fn: async (t) => {
const pri = PrivateKey.Generate();
const relay = await run({
admin: pri.toPublicKey(),
kv: await test_kv(),
default_policy: {
allowed_kinds: "all",
},
auth_required: true,
});
if (relay instanceof Error) fail(relay.message);
const admin = InMemoryAccountContext.FromString(pri.hex) as Signer;

await t.step("admin is always allowed", async () => {
const client = SingleRelayConnection.New(relay.ws_url, {
signer: admin,
});
const err = await client.newSub("", {});
assertNotInstanceOf(err, Error);
await client.close();
});

await t.step("a member is allowed", async () => {
const user = InMemoryAccountContext.Generate();
await relay.set_policy({
kind: NostrKind.TEXT_NOTE,
allow: new Set([user.publicKey.hex]),
});
const client = SingleRelayConnection.New(relay.ws_url, {
signer: user,
});
await sleep(10);
const err = await client.newSub("", {});
if (err instanceof Error) fail(err.message);
await client.close();
});

await t.step("stranger is blocked", async () => {
const client = SingleRelayConnection.New(relay.ws_url, {
signer: InMemoryAccountContext.Generate(),
});
await sleep(10);
const err = await client.newSub("", {});
assertIsError(err, Error);
await client.close();
});
await relay.shutdown();
},
});

async function randomEvent(ctx: InMemoryAccountContext, kind?: NostrKind, content?: string) {
const event = await prepareNormalNostrEvent(ctx, {
kind: kind || NostrKind.TEXT_NOTE,
Expand Down
8 changes: 4 additions & 4 deletions ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ async (req: Request, info: Deno.ServeHandlerInfo) => {
socket.onopen = ((socket: WebSocket) => async (e) => {
console.log("a client connected!", info.remoteAddr);
if (args.auth_required) {
console.log("check auth");
const url = new URL(req.url);
const auth = url.searchParams.get("auth");
if (auth == null || auth == "") {
Expand All @@ -71,12 +70,10 @@ async (req: Request, info: Deno.ServeHandlerInfo) => {
socket.close(3000, "invalid auth event format");
return;
}

const ok = await args.is_member(event.pubkey);
if (!ok) {
socket.close(3000, `pubkey ${event.pubkey} is not allowed`);
console.log("not allowed");
// @ts-ignore
// response.status = 500;
return;
}
}
Expand Down Expand Up @@ -296,6 +293,9 @@ async function handle_cmd_req(
// query this filter
for (const filter of filters) {
const event_candidates = await args.get_events_by_filter(filter);
if (event_candidates instanceof Error) {
return event_candidates;
}
for (const event of event_candidates) {
const policy = await args.resolvePolicyByKind(event.kind);
if (policy instanceof Error) {
Expand Down
Loading