fix: allow vault info under path scope and clarify API errors
Build and Push Docker Container / build-and-push (push) Successful in 4m29s

- Allow vault and vaults commands when a token has path restrictions

- Treat vault/vaults as pathless metadata commands in policy checks

- Add allowed commands, vaults and paths to 403 permission errors

- Surface real JWT failure reason in 401 instead of generic Unauthorized

- Report missing, malformed and empty Bearer tokens distinctly

- Catch httpRequest failures in the n8n node to avoid circular JSON

- Map API errors to clean NodeApiError with status code and detail

- Support continueOnFail with per-item error output in the n8n node

- Push a versioned image tag from pyproject in the CI workflow

- Bump server to 1.0.2 and n8n node package to 0.2.1

- Add tests for vault info access and richer auth error messages
This commit is contained in:
2026-06-24 11:41:18 +02:00
parent be78e3aa00
commit 3bda4002fe
9 changed files with 184 additions and 53 deletions
+9 -4
View File
@@ -73,12 +73,17 @@ def require_auth(request:Request) -> AuthContext|JSONResponse:
return error(500, "OBSIDIAN_JWT_SECRET is required")
header = request.headers.get("authorization", "")
if not header:
return error(401, "missing Authorization header; expected 'Bearer <jwt>'")
if not header.startswith("Bearer "):
return error(401, "Unauthorized")
return error(401, "invalid Authorization header; expected 'Bearer <jwt>'")
supplied = header.removeprefix("Bearer ").strip()
if not supplied:
return error(401, "empty Bearer token")
supplied = header.removeprefix("Bearer ")
try:
claims = decode_hs256_jwt(supplied, JWT_SECRET.encode())
return AuthContext(policy=policy_from_jwt_claims(supplied, claims))
except ValueError:
return error(401, "Unauthorized")
except ValueError as exc:
return error(401, f"invalid token: {exc}")
+21 -6
View File
@@ -36,7 +36,7 @@ PATH_REQUIRED_WHEN_RESTRICTED = {
"property:set", "property:remove", "property:read", "properties",
"aliases", "backlinks", "links", "outlinks", "tags", "tasks", "task",
}
PATHLESS_ALLOWED_WHEN_RESTRICTED = {"help", "version"}
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, ...]:
@@ -178,12 +178,18 @@ def authorize_command(policy:TokenPolicy|None, args:list[str], cwd:Path) -> None
command = target.command or ""
if not policy.allows_all_commands() and command not in policy.commands:
raise PermissionError(f"token is not allowed to run command: {command}")
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:
raise PermissionError(f"token is not allowed to access vault: {vault}")
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
@@ -192,13 +198,22 @@ def authorize_command(policy:TokenPolicy|None, args:list[str], cwd:Path) -> None
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("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}")
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]}")
raise PermissionError(
f"token is not allowed to access path: {denied[0]}. allowed paths: {allowed_paths}"
)