diff --git a/app/policy.py b/app/policy.py index 78531f7..a646787 100644 --- a/app/policy.py +++ b/app/policy.py @@ -29,7 +29,10 @@ class CommandTarget: vault:str|None paths:tuple[str, ...] -PATH_KEYS = {"path", "file", "folder", "name", "to"} +DEFAULT_PATH_KEYS = {"path", "file", "folder", "to"} +PATH_KEYS_BY_COMMAND = { + "rename": DEFAULT_PATH_KEYS | {"name"}, +} PATH_REQUIRED_WHEN_RESTRICTED = { "append", "prepend", "read", "create", "delete", "rename", "move", "file", "folder", "open", "diff", "search", "search:context", "files", "folders", @@ -100,6 +103,9 @@ def parse_kv_arg(arg:str) -> tuple[str, str]|None: return None return key, value +def path_keys_for_command(command:str|None) -> set[str]: + return PATH_KEYS_BY_COMMAND.get(command or "", DEFAULT_PATH_KEYS) + def extract_command_target(args:list[str]) -> CommandTarget: vault:str|None = None command:str|None = None @@ -113,12 +119,13 @@ def extract_command_target(args:list[str]) -> CommandTarget: command = arg break + path_keys = path_keys_for_command(command) for arg in args: parsed = parse_kv_arg(arg) if not parsed: continue key, value = parsed - if key in PATH_KEYS and value: + if key in path_keys and value: paths.append(value) return CommandTarget(command=command, vault=vault, paths=tuple(paths)) diff --git a/pyproject.toml b/pyproject.toml index 5dfcfde..4510df5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "obsidian-api" -version = "1.0.3" +version = "1.0.4" description = "HTTP API wrapper for running Obsidian CLI commands from n8n, SDKs, or agent harnesses." requires-python = ">=3.14" dependencies = [ diff --git a/tests/test_api.py b/tests/test_api.py index 7257170..fd003e3 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -157,6 +157,30 @@ def test_token_policy_restricts_path(monkeypatch): assert response.status_code == 403 assert response.json()["detail"].startswith("token is not allowed to access path: Private/Secret.md") +def test_property_set_name_is_not_treated_as_path(monkeypatch): + token = use_jwt_auth(monkeypatch, {"sub": "journal", "commands": ["property:set"], "vaults": ["*"], "paths": ["00. Journal/"]}) + + response = client.post( + "/commands", + headers={"Authorization": f"Bearer {token}"}, + json={"args": ["property:set", "path=00. Journal/Note.md", "name=from", "value=Daniel", "type=text"]}, + ) + + assert response.status_code == 200 + assert response.json()["command"] == ["python", "property:set", "path=00. Journal/Note.md", "name=from", "value=Daniel", "type=text"] + +def test_rename_name_is_treated_as_path(monkeypatch): + token = use_jwt_auth(monkeypatch, {"sub": "journal", "commands": ["rename"], "vaults": ["*"], "paths": ["00. Journal/"]}) + + response = client.post( + "/commands", + headers={"Authorization": f"Bearer {token}"}, + json={"args": ["rename", "path=00. Journal/Old.md", "name=Private/New.md"]}, + ) + + assert response.status_code == 403 + assert response.json()["detail"].startswith("token is not allowed to access path: Private/New.md") + def test_token_policy_allows_allowed_path(monkeypatch): token = use_jwt_auth(monkeypatch, {"sub": "inbox", "commands": ["read"], "vaults": ["*"], "paths": ["Inbox/"]}) diff --git a/uv.lock b/uv.lock index 5dac073..00e5ead 100644 --- a/uv.lock +++ b/uv.lock @@ -123,7 +123,7 @@ wheels = [ [[package]] name = "obsidian-api" -version = "1.0.3" +version = "1.0.4" source = { virtual = "." } dependencies = [ { name = "starlette" },