202 lines
8.9 KiB
Markdown
202 lines
8.9 KiB
Markdown
# Multi-Admin Support Plan
|
|
|
|
## Overview
|
|
|
|
Enable multiple Nostr pubkeys to act as relay administrators. Currently the relay stores a single `admin_pubkey` string in the `config` table and uses `strcmp()` equality checks throughout the codebase. This plan converts that to a comma-separated list of pubkeys stored in the same config key, with a centralized `is_admin_pubkey()` helper that all authorization gates call.
|
|
|
|
## Design Principles
|
|
|
|
1. **Minimal schema change** — keep using the existing `config` table with key `admin_pubkey`. The value changes from a single 64-char hex string to a comma-separated list of 64-char hex strings.
|
|
2. **Primary admin concept** — the first pubkey in the list is the "primary admin" (the original one). Only the primary admin can add/remove other admins. This prevents privilege escalation.
|
|
3. **Centralized check** — introduce one function `is_admin_pubkey(const char* pubkey)` that all 8 authorization gates call instead of inline `strcmp()`.
|
|
4. **In-memory cache** — parse the comma-separated list once on startup/config-change into a static array for O(1)-ish lookups without repeated string parsing.
|
|
5. **Backward compatible** — a single pubkey with no commas works identically to today.
|
|
|
|
## Data Model
|
|
|
|
```
|
|
config table:
|
|
key = "admin_pubkey"
|
|
value = "hex1,hex2,hex3" (comma-separated, no spaces)
|
|
data_type = "string"
|
|
```
|
|
|
|
Maximum admins: 10 (compile-time constant `MAX_ADMIN_PUBKEYS`).
|
|
|
|
## Architecture
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[Config Table: admin_pubkey = hex1,hex2,hex3] --> B[reload_admin_pubkeys_cache]
|
|
B --> C[Static array: g_admin_pubkeys with count]
|
|
D[Any authorization check] --> E[is_admin_pubkey - pubkey]
|
|
E --> C
|
|
F[CLI: -a flag] --> G[first_time_startup_sequence]
|
|
G --> A
|
|
H[Admin command: add_admin / remove_admin] --> I[modify_admin_list]
|
|
I --> A
|
|
I --> B
|
|
```
|
|
|
|
## Implementation Steps
|
|
|
|
### Step 1: Add centralized admin check infrastructure in config.c / config.h
|
|
|
|
**New constants and types:**
|
|
```c
|
|
#define MAX_ADMIN_PUBKEYS 10
|
|
```
|
|
|
|
**New functions in config.h:**
|
|
```c
|
|
int is_admin_pubkey(const char* pubkey); // Returns 1 if pubkey is in admin list
|
|
int get_admin_pubkey_count(void); // Returns number of admins
|
|
const char* get_primary_admin_pubkey(void); // Returns first admin pubkey
|
|
int reload_admin_pubkeys_cache(void); // Parse config value into cache
|
|
int add_admin_pubkey(const char* pubkey); // Add pubkey to admin list (primary admin only)
|
|
int remove_admin_pubkey(const char* pubkey); // Remove pubkey from admin list (primary admin only)
|
|
```
|
|
|
|
**Implementation in config.c:**
|
|
- Static array `g_admin_pubkeys[MAX_ADMIN_PUBKEYS][65]` and `g_admin_pubkey_count`
|
|
- `reload_admin_pubkeys_cache()` reads `admin_pubkey` from config table, splits on commas, validates each is 64 hex chars, populates array
|
|
- `is_admin_pubkey()` iterates the cached array with `strcmp()`
|
|
- `get_primary_admin_pubkey()` returns `g_admin_pubkeys[0]` — used where only the primary admin matters (WoT sync, etc.)
|
|
- `add_admin_pubkey()` / `remove_admin_pubkey()` modify the comma-separated string in the config table and call `reload_admin_pubkeys_cache()`
|
|
|
|
### Step 2: Replace all 8 authorization strcmp gates
|
|
|
|
Each of these currently does:
|
|
```c
|
|
const char* admin_pubkey = get_config_value("admin_pubkey");
|
|
if (strcmp(sender_pubkey, admin_pubkey) == 0) { /* authorized */ }
|
|
```
|
|
|
|
Replace with:
|
|
```c
|
|
if (is_admin_pubkey(sender_pubkey)) { /* authorized */ }
|
|
```
|
|
|
|
**Files and locations:**
|
|
|
|
| File | Function | Line | Change |
|
|
|------|----------|------|--------|
|
|
| src/config.c | `process_admin_config_event()` | ~1441 | Replace strcmp with `is_admin_pubkey()` |
|
|
| src/config.c | `handle_kind_23456_unified()` | ~2727 | Replace strcmp with `is_admin_pubkey()` |
|
|
| src/main.c | `verify_admin_event()` | ~1905 | Replace strcmp with `is_admin_pubkey()` |
|
|
| src/main.c | Event storage / WoT trigger | ~1174 | Replace strcmp with `is_admin_pubkey()` |
|
|
| src/websockets.c | NIP-42 auth bypass | ~2324 | Replace strcmp with `is_admin_pubkey()` |
|
|
| src/websockets.c | NIP-17 DM admin check | ~3560 | Replace strcmp with `is_admin_pubkey()` |
|
|
| src/dm_admin.c | `process_admin_dm()` | ~503 | Replace strcmp with `is_admin_pubkey()` |
|
|
| src/request_validator.c | Event validation bypass | ~307 | Replace strcmp with `is_admin_pubkey()` |
|
|
|
|
Each replacement also eliminates the `get_config_value("admin_pubkey")` call and its associated `free()`, simplifying the code.
|
|
|
|
### Step 3: Update startup / initialization pipeline
|
|
|
|
**config.c `first_time_startup_sequence()`:**
|
|
- No change needed — it already stores a single pubkey. The comma-separated format with one entry is identical to the current format.
|
|
|
|
**config.c `populate_all_config_values_atomic()`:**
|
|
- No change needed — inserts the initial admin_pubkey as a single value.
|
|
|
|
**config.c `add_pubkeys_to_config_table()`:**
|
|
- No change needed — stores single pubkey on migration.
|
|
|
|
**config.c `apply_cli_overrides_atomic()`:**
|
|
- No change needed — CLI override replaces the entire admin_pubkey value.
|
|
|
|
**main.c startup validation:**
|
|
- Update the pre-warm validation at ~line 2438 to call `reload_admin_pubkeys_cache()` and validate that at least one admin pubkey exists, rather than checking for exactly 64 chars.
|
|
|
|
**Call `reload_admin_pubkeys_cache()`** after:
|
|
- `populate_all_config_values_atomic()` completes
|
|
- `reload_config_from_table()` completes
|
|
- Any admin add/remove operation
|
|
|
|
### Step 4: Add admin management commands to kind 23456 API
|
|
|
|
Add two new admin commands that can be sent as encrypted kind 23456 events:
|
|
|
|
```json
|
|
["add_admin", "pubkey_hex"]
|
|
["remove_admin", "pubkey_hex"]
|
|
["list_admins"]
|
|
```
|
|
|
|
**Authorization rule:** Only the primary admin (first in list) can add/remove other admins. This is enforced in the command handler, not in the general `is_admin_pubkey()` check.
|
|
|
|
**Implementation location:** `handle_system_command_unified()` in config.c, alongside existing commands like `system_status`, `graceful_shutdown`, etc.
|
|
|
|
### Step 5: Update WoT sync to handle multiple admins
|
|
|
|
In `wot_sync_from_admin_kind3()`:
|
|
- Currently queries kind 3 events from the single admin pubkey
|
|
- Change to use `get_primary_admin_pubkey()` — WoT trust anchor should remain the primary admin
|
|
- This is a deliberate design choice: secondary admins can manage the relay but the WoT trust graph is rooted in the primary admin's social graph
|
|
|
|
### Step 6: Update CLI to accept multiple admin pubkeys
|
|
|
|
**Option A - Comma-separated single flag (recommended):**
|
|
```bash
|
|
./c_relay_pg -a npub1...,npub2...,npub3...
|
|
```
|
|
|
|
**Changes:**
|
|
- `cli_options_t.admin_pubkey_override` becomes `char admin_pubkeys_override[MAX_ADMIN_PUBKEYS * 65]` (larger buffer for comma-separated list)
|
|
- CLI parsing in main.c splits on commas, decodes each npub/hex, reassembles as comma-separated hex
|
|
- `make_and_restart_relay.sh` `-a` flag documentation updated
|
|
|
|
### Step 7: Update config validation in api.c
|
|
|
|
In the config validation table at line ~912:
|
|
```c
|
|
{"admin_pubkey", "string", 0, 65},
|
|
```
|
|
Change max length to accommodate multiple pubkeys:
|
|
```c
|
|
{"admin_pubkey", "string", 0, MAX_ADMIN_PUBKEYS * 65},
|
|
```
|
|
|
|
### Step 8: Update get_admin_pubkey_cached
|
|
|
|
`get_admin_pubkey_cached()` currently returns the raw config value. Update to return only the primary admin pubkey for backward compatibility with any callers that expect a single 64-char string.
|
|
|
|
### Step 9: Update test scripts
|
|
|
|
Update test scripts that hardcode admin keys to work with the multi-admin system:
|
|
- `tests/sql_test.sh`
|
|
- `tests/white_black_test.sh`
|
|
- `tests/17_nip_test.sh`
|
|
- `tests/run_all_tests.sh`
|
|
- `tests/.test_keys.txt`
|
|
|
|
Add new test cases for:
|
|
- Adding a second admin via kind 23456 command
|
|
- Verifying second admin can send admin commands
|
|
- Verifying non-admin cannot send admin commands
|
|
- Removing a secondary admin
|
|
- Verifying primary admin cannot be removed
|
|
- Verifying secondary admin cannot add/remove admins
|
|
|
|
### Step 10: Update documentation
|
|
|
|
- `AGENTS.md` — update admin API event structure section
|
|
- `docs/configuration_guide.md` — document multi-admin setup
|
|
- `docs/user_guide.md` — document admin management commands
|
|
- `API.md` — document new add_admin/remove_admin/list_admins commands
|
|
- `README.md` — mention multi-admin capability
|
|
|
|
## Migration / Backward Compatibility
|
|
|
|
- Existing databases with a single `admin_pubkey` value work without any migration — a string with no commas is treated as a single-admin list
|
|
- The `is_admin_pubkey()` function handles both formats transparently
|
|
- No schema changes needed — the config table already stores strings of arbitrary length
|
|
|
|
## Security Considerations
|
|
|
|
1. **Primary admin privilege** — only the first pubkey in the list can add/remove admins, preventing secondary admins from locking out the primary
|
|
2. **No self-removal** — the primary admin cannot remove themselves (prevents accidental lockout)
|
|
3. **Max admin limit** — capped at 10 to prevent abuse and keep the linear scan fast
|
|
4. **Validation** — each pubkey in the list is validated as 64 hex characters before storage
|