From 3152dd8a5c35fc0a0b9020385dc357321a3fa316 Mon Sep 17 00:00:00 2001 From: Fuu Date: Fri, 15 May 2026 21:00:57 +0800 Subject: [PATCH] feat: support 3D-RBAC stateful token authentication --- .dockerignore | 11 +++++++++++ Dockerfile | 18 ++++++++++++++++++ README.md | 25 +++++++++++++++++++++---- config.yaml | 1 + docker-compose.yml | 13 +++++++++++++ fns_cli/__init__.py | 2 +- fns_cli/client.py | 17 +++++++++++++++-- fns_cli/config.py | 2 ++ tests/test_client.py | 1 + 9 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8b71b9d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.git +.github +__pycache__ +*.pyc +tests/ +doc/ +vault/ +config/ +.fns_state.json +*.md +!fns_cli/*.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2223405 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.13-slim + +WORKDIR /app + +# Install dependencies first for better layer caching +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY fns_cli/ ./fns_cli/ + +# Default config mount point +VOLUME ["/app/config", "/app/vault"] + +ENV PYTHONUNBUFFERED=1 + +ENTRYPOINT ["python", "-m", "fns_cli.main"] +CMD ["run", "-c", "/app/config/config.yaml"] diff --git a/README.md b/README.md index 2423d77..21d367b 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Edit `config.yaml`: ```yaml server: api: "https://your-server-address" # Fast Note Sync Service base URL - token: "your_api_token" # API token from the admin panel + token: "your_api_token" # JWT token from the Token Management panel vault: "notes" # Vault name; must match the Obsidian plugin sync: @@ -85,6 +85,7 @@ client: reconnect_max_retries: 15 reconnect_base_delay: 3 heartbeat_interval: 30 + client_type: "fns-cli" # Client identifier for 3D-RBAC auth logging: level: "INFO" @@ -93,16 +94,32 @@ logging: **How to obtain a token** +> **Note:** Since service version with 3D-RBAC, tokens are now JWT-based and managed via the web UI. + 1. Open the Fast Note Sync Service web UI (e.g. `https://your-server-address`) 2. Sign in -3. Click **"Copy API Config"** -4. Copy `api`, `apiToken`, and `vault` from the JSON into `config.yaml` +3. Navigate to **Settings → Token Management** +4. Click **"Create Token"** with these settings: + - **Client Type**: `fns-cli` (or a descriptive name like `my-server-cli`) + - **Scope**: `p:ws c:fns-cli* f:note_rw,file_rw,config_rw` (full read/write access via WebSocket) + - **Expired Days**: set as needed (e.g. `365`) +5. Copy the generated JWT token into `config.yaml`'s `token` field + +**Scope examples:** + +| Use case | Scope | +|----------|-------| +| Full sync (read + write) | `p:ws c:fns-cli* f:note_rw,file_rw,config_rw` | +| Read-only (pull only) | `p:ws c:fns-cli* f:note_r,file_r,config_r` | +| Notes only | `p:ws c:fns-cli* f:note_rw` | + +> **Fallback:** If your server version still supports legacy tokens, the old "Copy API Config" method also works — the CLI sends the token as-is. Optional environment variables (override when not set in the file): ```bash export FNS_API="https://your-server-address" -export FNS_TOKEN="your_api_token" +export FNS_TOKEN="your_jwt_token" ``` ### 4. Run diff --git a/config.yaml b/config.yaml index b75ed9a..9f4fe8e 100644 --- a/config.yaml +++ b/config.yaml @@ -18,6 +18,7 @@ client: reconnect_max_retries: 15 reconnect_base_delay: 3 heartbeat_interval: 30 + client_type: "fns-cli" logging: level: "INFO" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..683fd5d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +services: + fns-cli: + build: . + container_name: fns-cli + restart: unless-stopped + volumes: + - ./config:/app/config # Mount config directory + - ./vault:/app/vault # Mount vault directory for sync + environment: + - PYTHONUNBUFFERED=1 + # Optional: override config values via env vars + # - FNS_API=https://your-server-address + # - FNS_TOKEN=your_jwt_token diff --git a/fns_cli/__init__.py b/fns_cli/__init__.py index 87abe33..085e8cc 100644 --- a/fns_cli/__init__.py +++ b/fns_cli/__init__.py @@ -1,3 +1,3 @@ """FastNodeSync CLI - A command-line client for Fast Note Sync Service.""" -__version__ = "0.1.0" +__version__ = "0.2.0" diff --git a/fns_cli/client.py b/fns_cli/client.py index 5c975c9..8aaf18e 100644 --- a/fns_cli/client.py +++ b/fns_cli/client.py @@ -4,6 +4,7 @@ import asyncio import logging +import sys from typing import Any, Callable, Coroutine import websockets @@ -18,6 +19,7 @@ decode_message, parse_binary_chunk, ) +from . import __version__ log = logging.getLogger("fns_cli.client") @@ -128,9 +130,13 @@ async def _connect(self) -> None: self._ensure_ready_event().clear() self._connect_count += 1 + client_type = self.config.client.client_type url = ( f"{self.config.ws_api}/api/user/sync" f"?lang=zh-cn&count={self._connect_count}" + f"&client={client_type}" + f"&clientName=FastNodeSync-CLI" + f"&clientVersion={__version__}" ) log.info("Connecting to %s", url) @@ -197,8 +203,15 @@ async def _on_auth_response(self, msg: WSMessage) -> None: client_info = WSMessage(ACTION_CLIENT_INFO, { "name": "FastNodeSync-CLI", - "version": "0.1.0", - "type": "cli", + "version": __version__, + "type": self.config.client.client_type, + "isDesktop": True, + "isMobile": False, + "isPhone": False, + "isTablet": False, + "isMacOS": sys.platform == "darwin", + "isWin": sys.platform == "win32", + "isLinux": sys.platform.startswith("linux"), }) await self._raw_send(client_info.encode()) await self._flush_queue() diff --git a/fns_cli/config.py b/fns_cli/config.py index 13baa04..68c9665 100644 --- a/fns_cli/config.py +++ b/fns_cli/config.py @@ -38,6 +38,7 @@ class ClientConfig: reconnect_max_retries: int = 15 reconnect_base_delay: int = 3 heartbeat_interval: int = 30 + client_type: str = "fns-cli" @dataclass @@ -108,6 +109,7 @@ def load_config(path: str) -> AppConfig: reconnect_max_retries=c.get("reconnect_max_retries", 15), reconnect_base_delay=c.get("reconnect_base_delay", 3), heartbeat_interval=c.get("heartbeat_interval", 30), + client_type=c.get("client_type", "fns-cli"), ) if "logging" in raw: diff --git a/tests/test_client.py b/tests/test_client.py index 72b6fad..732248c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -21,6 +21,7 @@ def _make_config() -> MagicMock: config = MagicMock() config.client.reconnect_base_delay = 1 config.client.reconnect_max_retries = 3 + config.client.client_type = "fns-cli" config.server.token = "token" config.ws_api = "wss://example.com" return config