Files
obsidian-api/app/policy.py
T
daniel156161 c34dab2cca
Build and Push Docker Container / build-and-push (push) Successful in 4m28s
feat(properties): prepare property commands before execution
- Create missing note files before running property:set.
- Default date-only datetime values to midnight.
- Keep property names out of path authorization checks.
- Split policy, vault, daily note, and command target helpers.
- Split n8n node args, description, output, and API response helpers.
- Split API tests by auth, command, daily note, and property behavior.
- Bump API and n8n node versions.
2026-06-24 14:31:42 +02:00

93 lines
3.3 KiB
Python

from dataclasses import dataclass
from pathlib import Path
from typing import Any
from app.command_target import CommandTarget, extract_command_target, parse_kv_arg
from app.config import DEFAULT_VAULT_NAME
from app.daily_notes import daily_note_path_for_vault
from app.path_policy import is_path_allowed, normalize_policy_path
from app.vaults import vault_for_path, vault_root_for_target
@dataclass(frozen=True)
class TokenPolicy:
token:str = ""
name:str = "jwt"
vaults:tuple[str, ...] = ("*",)
paths:tuple[str, ...] = ("*",)
commands:tuple[str, ...] = ("*",)
def allows_all_vaults(self) -> bool:
return "*" in self.vaults
def allows_all_paths(self) -> bool:
return "*" in self.paths
def allows_all_commands(self) -> bool:
return "*" in self.commands
PATH_REQUIRED_WHEN_RESTRICTED = {
"append", "prepend", "read", "create", "delete", "rename", "move", "file", "folder", "open", "diff",
"search", "search:context", "files", "folders",
"property:set", "property:remove", "property:read", "properties",
"aliases", "backlinks", "links", "outlinks", "tags", "tasks", "task",
}
PATHLESS_ALLOWED_WHEN_RESTRICTED = {"help", "version", "vault", "vaults"}
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:
return default
if isinstance(value, str):
return (value,)
if isinstance(value, list) and all(isinstance(item, str) for item in value):
return tuple(value)
raise ValueError("JWT claims vaults, paths and commands must be strings or string arrays")
def authorize_command(policy:TokenPolicy|None, args:list[str], cwd:Path) -> None:
if policy is None:
return
target = extract_command_target(args)
command = target.command or ""
if not policy.allows_all_commands() and command not in policy.commands:
allowed = ", ".join(sorted(policy.commands)) or "(none)"
raise PermissionError(
f"token is not allowed to run command: {command or '(empty)'}. allowed commands: {allowed}"
)
if not policy.allows_all_vaults():
vault = target.vault or vault_for_path(cwd) or DEFAULT_VAULT_NAME
if vault not in policy.vaults:
allowed = ", ".join(sorted(policy.vaults)) or "(none)"
raise PermissionError(
f"token is not allowed to access vault: {vault}. allowed vaults: {allowed}"
)
if policy.allows_all_paths():
return
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)))
allowed_paths = ", ".join(policy.paths) or "(none)"
if not paths:
if command in PATHLESS_ALLOWED_WHEN_RESTRICTED:
return
if command in PATH_REQUIRED_WHEN_RESTRICTED:
raise PermissionError(
f"token has path restrictions; command '{command}' must include an allowed "
f"path/file/folder/name argument. allowed paths: {allowed_paths}"
)
raise PermissionError(
f"token has path restrictions; command '{command}' is not allowed without an "
f"explicit path. allowed paths: {allowed_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]}. allowed paths: {allowed_paths}"
)