feat: resolve daily-note path from Obsidian config for scoped tokens
Build and Push Docker Container / build-and-push (push) Successful in 4m15s
Build and Push Docker Container / build-and-push (push) Successful in 4m15s
- Add daily_note_path_for_vault to read .obsidian/daily-notes.json - Honor configured folder and date format for daily commands - Add moment_to_strftime to translate Obsidian date tokens - Authorize pathless daily:* commands against the resolved path - Pick vault root from token vault, cwd, or default vault - Normalize n8n httpRequest response to avoid circular JSON output - Force returnFullResponse false and parse body to plain result - Bump n8n-nodes-obsidian-cli-api to 0.2.0 - Add tests for allowed and denied scoped daily commands
This commit is contained in:
+56
-2
@@ -1,6 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
import json
|
||||
|
||||
from app.config import DEFAULT_VAULT_NAME, REGISTERED_VAULTS
|
||||
|
||||
@@ -35,6 +37,7 @@ PATH_REQUIRED_WHEN_RESTRICTED = {
|
||||
"aliases", "backlinks", "links", "outlinks", "tags", "tasks", "task",
|
||||
}
|
||||
PATHLESS_ALLOWED_WHEN_RESTRICTED = {"help", "version"}
|
||||
DAILY_NOTE_COMMANDS = {"daily:path", "daily:read", "daily:append", "daily:prepend"}
|
||||
|
||||
def tuple_claim(value:Any, default:tuple[str, ...]) -> tuple[str, ...]:
|
||||
if value is None:
|
||||
@@ -120,6 +123,53 @@ def extract_command_target(args:list[str]) -> CommandTarget:
|
||||
|
||||
return CommandTarget(command=command, vault=vault, paths=tuple(paths))
|
||||
|
||||
def moment_to_strftime(format_string:str) -> str:
|
||||
replacements = [
|
||||
("YYYY", "%Y"),
|
||||
("YY", "%y"),
|
||||
("MMMM", "%B"),
|
||||
("MMM", "%b"),
|
||||
("MM", "%m"),
|
||||
("DDDD", "%j"),
|
||||
("DD", "%d"),
|
||||
("dddd", "%A"),
|
||||
("ddd", "%a"),
|
||||
]
|
||||
converted = format_string or "YYYY-MM-DD"
|
||||
for moment_token, strftime_token in replacements:
|
||||
converted = converted.replace(moment_token, strftime_token)
|
||||
return converted
|
||||
|
||||
def daily_note_path_for_vault(vault_root:Path, now:datetime|None = None) -> str:
|
||||
settings_path = vault_root / ".obsidian" / "daily-notes.json"
|
||||
folder = ""
|
||||
format_string = "YYYY-MM-DD"
|
||||
|
||||
if settings_path.exists() and settings_path.is_file():
|
||||
try:
|
||||
settings = json.loads(settings_path.read_text(encoding="utf-8"))
|
||||
if isinstance(settings, dict):
|
||||
folder_value = settings.get("folder")
|
||||
format_value = settings.get("format")
|
||||
if isinstance(folder_value, str):
|
||||
folder = folder_value.strip().strip("/")
|
||||
if isinstance(format_value, str) and format_value.strip():
|
||||
format_string = format_value.strip()
|
||||
except (OSError, json.JSONDecodeError):
|
||||
pass
|
||||
|
||||
note_name = (now or datetime.now()).strftime(moment_to_strftime(format_string))
|
||||
if not note_name.endswith(".md"):
|
||||
note_name += ".md"
|
||||
return f"{folder}/{note_name}" if folder else note_name
|
||||
|
||||
def vault_root_for_target(target:CommandTarget, cwd:Path) -> Path:
|
||||
if target.vault and target.vault in REGISTERED_VAULTS:
|
||||
return Path(REGISTERED_VAULTS[target.vault]).resolve()
|
||||
|
||||
vault_name = vault_for_path(cwd) or DEFAULT_VAULT_NAME
|
||||
return Path(REGISTERED_VAULTS.get(vault_name, cwd)).resolve()
|
||||
|
||||
def authorize_command(policy:TokenPolicy|None, args:list[str], cwd:Path) -> None:
|
||||
if policy is None:
|
||||
return
|
||||
@@ -138,13 +188,17 @@ def authorize_command(policy:TokenPolicy|None, args:list[str], cwd:Path) -> None
|
||||
if policy.allows_all_paths():
|
||||
return
|
||||
|
||||
if not target.paths:
|
||||
paths = list(target.paths)
|
||||
if not paths and command in DAILY_NOTE_COMMANDS:
|
||||
paths.append(daily_note_path_for_vault(vault_root_for_target(target, cwd)))
|
||||
|
||||
if not paths:
|
||||
if command in PATHLESS_ALLOWED_WHEN_RESTRICTED:
|
||||
return
|
||||
if command in PATH_REQUIRED_WHEN_RESTRICTED:
|
||||
raise PermissionError("token has path restrictions; command must include an allowed path/file/folder/name argument")
|
||||
raise PermissionError(f"token has path restrictions; command is not allowed without an explicit path: {command}")
|
||||
|
||||
denied = [path for path in target.paths if not is_path_allowed(path, policy.paths)]
|
||||
denied = [path for path in paths if not is_path_allowed(path, policy.paths)]
|
||||
if denied:
|
||||
raise PermissionError(f"token is not allowed to access path: {denied[0]}")
|
||||
|
||||
Reference in New Issue
Block a user