generated from coulomb/repo-seed
Access controlled knowledge gateway functionality
This commit is contained in:
@@ -52,6 +52,7 @@ from markitect_tool.generation import (
|
||||
from markitect_tool.literate import tangle_markdown, weave_markdown, write_tangle_files
|
||||
from markitect_tool.ops import IncludeError, compose_files, resolve_includes, transform_markdown
|
||||
from markitect_tool.processor import ProcessorContext, run_fenced_processors
|
||||
from markitect_tool.policy import LocalLabelPolicyGateway
|
||||
from markitect_tool.query import (
|
||||
InvalidQueryError,
|
||||
extract_document,
|
||||
@@ -727,6 +728,69 @@ def backend_refresh_plan(
|
||||
raise click.exceptions.Exit(1 if plan.dirty else 0)
|
||||
|
||||
|
||||
@main.group()
|
||||
def policy() -> None:
|
||||
"""Check local access policy decisions."""
|
||||
|
||||
|
||||
@policy.command("check")
|
||||
@click.argument("subject")
|
||||
@click.argument("action")
|
||||
@click.argument("object_id")
|
||||
@click.option(
|
||||
"--policy",
|
||||
"policy_file",
|
||||
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
||||
help="Local label policy file.",
|
||||
)
|
||||
@click.option("--label", "labels", multiple=True, help="Object policy label. May be repeated.")
|
||||
@click.option("--path", "object_path", help="Object path for path ACL and path-label rules.")
|
||||
@click.option("--trust-zone", help="Object trust zone.")
|
||||
@click.option(
|
||||
"--policy-mode",
|
||||
type=click.Choice(["off", "audit", "enforce"], case_sensitive=False),
|
||||
help="Override policy mode for this check.",
|
||||
)
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
|
||||
default="text",
|
||||
show_default=True,
|
||||
)
|
||||
def policy_check(
|
||||
subject: str,
|
||||
action: str,
|
||||
object_id: str,
|
||||
policy_file: Path | None,
|
||||
labels: tuple[str, ...],
|
||||
object_path: str | None,
|
||||
trust_zone: str | None,
|
||||
policy_mode: str | None,
|
||||
output_format: str,
|
||||
) -> None:
|
||||
"""Authorize one subject/action/object tuple with local label policy."""
|
||||
|
||||
try:
|
||||
gateway = _load_policy_gateway(policy_file, policy_mode) or LocalLabelPolicyGateway()
|
||||
decision = gateway.authorize(
|
||||
subject,
|
||||
action,
|
||||
object_id,
|
||||
context={
|
||||
"object": {
|
||||
"labels": list(labels),
|
||||
"path": object_path,
|
||||
"trust_zone": trust_zone,
|
||||
}
|
||||
},
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
_emit_policy_result({"decision": decision}, output_format)
|
||||
raise click.exceptions.Exit(0 if decision.get("allowed") else 1)
|
||||
|
||||
|
||||
@main.group("class")
|
||||
def class_group() -> None:
|
||||
"""Resolve deterministic content classes."""
|
||||
@@ -1028,6 +1092,18 @@ def cache_index(
|
||||
multiple=True,
|
||||
help="Restrict query to one or more indexed relative paths.",
|
||||
)
|
||||
@click.option(
|
||||
"--policy",
|
||||
"policy_file",
|
||||
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
||||
help="Local label policy file used to filter results.",
|
||||
)
|
||||
@click.option("--subject", default="anonymous", help="Policy subject id.")
|
||||
@click.option(
|
||||
"--policy-mode",
|
||||
type=click.Choice(["off", "audit", "enforce"], case_sensitive=False),
|
||||
help="Override policy mode for this query.",
|
||||
)
|
||||
@click.option(
|
||||
"--engine",
|
||||
type=click.Choice(["selector", "jsonpath"], case_sensitive=False),
|
||||
@@ -1047,17 +1123,22 @@ def cache_query(
|
||||
root: Path,
|
||||
index_path: Path | None,
|
||||
paths: tuple[str, ...],
|
||||
policy_file: Path | None,
|
||||
subject: str,
|
||||
policy_mode: str | None,
|
||||
engine: str,
|
||||
output_format: str,
|
||||
) -> None:
|
||||
"""Run a selector or JSONPath query over indexed document snapshots."""
|
||||
|
||||
store = LocalSnapshotStore(local_index_path_for(root, index_path))
|
||||
policy_gateway = _load_policy_gateway(policy_file, policy_mode)
|
||||
indexed_paths = sorted(paths or [state.path for state in store.load_state()])
|
||||
all_matches = []
|
||||
try:
|
||||
for indexed_path in indexed_paths:
|
||||
document = Document.from_dict(store.get_document(indexed_path))
|
||||
policy_metadata = store.policy_metadata(indexed_path) if policy_gateway else {}
|
||||
matches = (
|
||||
query_document_jsonpath(document, selector)
|
||||
if engine == "jsonpath"
|
||||
@@ -1066,11 +1147,17 @@ def cache_query(
|
||||
for match in matches:
|
||||
item = match.to_dict()
|
||||
item["source_path"] = indexed_path
|
||||
if policy_metadata:
|
||||
item["policy"] = policy_metadata
|
||||
all_matches.append(item)
|
||||
except KeyError as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
except InvalidQueryError as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
policy_result = None
|
||||
if policy_gateway:
|
||||
policy_result = policy_gateway.filter_results(subject, "query", all_matches)
|
||||
all_matches = policy_result["results"]
|
||||
data = {
|
||||
"selector": selector,
|
||||
"engine": engine,
|
||||
@@ -1078,6 +1165,10 @@ def cache_query(
|
||||
"count": len(all_matches),
|
||||
"matches": all_matches,
|
||||
}
|
||||
if policy_result:
|
||||
data["policy"] = policy_result.get("policy")
|
||||
data["policy_decisions"] = policy_result.get("decisions")
|
||||
data["diagnostics"] = policy_result.get("diagnostics")
|
||||
_emit_query(data, output_format)
|
||||
|
||||
|
||||
@@ -1096,6 +1187,18 @@ def cache_query(
|
||||
help="SQLite index path. Defaults to .markitect/cache/index.sqlite3 under root.",
|
||||
)
|
||||
@click.option("--limit", type=int, default=20, show_default=True)
|
||||
@click.option(
|
||||
"--policy",
|
||||
"policy_file",
|
||||
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
||||
help="Local label policy file used to filter results.",
|
||||
)
|
||||
@click.option("--subject", default="anonymous", help="Policy subject id.")
|
||||
@click.option(
|
||||
"--policy-mode",
|
||||
type=click.Choice(["off", "audit", "enforce"], case_sensitive=False),
|
||||
help="Override policy mode for this search.",
|
||||
)
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
@@ -1108,21 +1211,39 @@ def search(
|
||||
root: Path,
|
||||
index_path: Path | None,
|
||||
limit: int,
|
||||
policy_file: Path | None,
|
||||
subject: str,
|
||||
policy_mode: str | None,
|
||||
output_format: str,
|
||||
) -> None:
|
||||
"""Search the local SQLite index with FTS5."""
|
||||
|
||||
try:
|
||||
store = LocalSnapshotStore(local_index_path_for(root, index_path))
|
||||
results = store.search(text, limit=limit)
|
||||
policy_gateway = _load_policy_gateway(policy_file, policy_mode)
|
||||
if policy_gateway:
|
||||
policy_result = store.search_with_policy(
|
||||
text,
|
||||
subject=subject,
|
||||
gateway=policy_gateway,
|
||||
limit=limit,
|
||||
)
|
||||
matches = policy_result["results"]
|
||||
else:
|
||||
policy_result = None
|
||||
matches = [result.to_dict() for result in store.search(text, limit=limit)]
|
||||
except ValueError as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
data = {
|
||||
"query": text,
|
||||
"index_path": str(local_index_path_for(root, index_path)),
|
||||
"count": len(results),
|
||||
"matches": [result.to_dict() for result in results],
|
||||
"count": len(matches),
|
||||
"matches": matches,
|
||||
}
|
||||
if policy_result:
|
||||
data["policy"] = policy_result.get("policy")
|
||||
data["policy_decisions"] = policy_result.get("decisions")
|
||||
data["diagnostics"] = policy_result.get("diagnostics")
|
||||
_emit_search_results(data, output_format)
|
||||
|
||||
|
||||
@@ -1529,6 +1650,20 @@ def contract_form_state(
|
||||
raise click.exceptions.Exit(0 if form_state.valid else 1)
|
||||
|
||||
|
||||
def _load_policy_gateway(
|
||||
policy_file: Path | None,
|
||||
policy_mode: str | None,
|
||||
) -> LocalLabelPolicyGateway | None:
|
||||
if policy_file is None and policy_mode is None:
|
||||
return None
|
||||
try:
|
||||
if policy_file:
|
||||
return LocalLabelPolicyGateway.from_file(policy_file, mode=policy_mode)
|
||||
return LocalLabelPolicyGateway(mode=policy_mode)
|
||||
except ValueError as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
|
||||
|
||||
def _emit_result(data: dict, output_format: str) -> None:
|
||||
if output_format == "json":
|
||||
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
@@ -1588,6 +1723,19 @@ def _emit_form_state(data: dict, output_format: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _emit_policy_result(data: dict, output_format: str) -> None:
|
||||
if output_format == "json":
|
||||
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
elif output_format == "yaml":
|
||||
click.echo(yaml.safe_dump(data, sort_keys=False))
|
||||
else:
|
||||
decision = data["decision"]
|
||||
click.echo("allowed" if decision.get("allowed") else "denied")
|
||||
click.echo(f"effect: {decision.get('effect')}")
|
||||
click.echo(f"decision_id: {decision.get('decision_id')}")
|
||||
click.echo(f"reason: {decision.get('reason')}")
|
||||
|
||||
|
||||
def _emit_metrics(data: dict, output_format: str) -> None:
|
||||
if output_format == "json":
|
||||
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
@@ -1615,11 +1763,15 @@ def _emit_query(data: dict, output_format: str) -> None:
|
||||
click.echo(yaml.safe_dump(data, sort_keys=False))
|
||||
else:
|
||||
click.echo(f"{data['count']} match(es)")
|
||||
if data.get("policy"):
|
||||
_emit_policy_summary(data["policy"])
|
||||
for match in data["matches"]:
|
||||
location = f":{match['line']}" if match.get("line") else ""
|
||||
click.echo(f"- {match['kind']} {match['path']}{location}")
|
||||
if match.get("text"):
|
||||
click.echo(f" {match['text'].splitlines()[0]}")
|
||||
for diagnostic in data.get("diagnostics", []):
|
||||
click.echo(f"! [{diagnostic['severity']}] {diagnostic['code']}: {diagnostic['message']}")
|
||||
|
||||
|
||||
def _emit_extract(data: dict, output_format: str) -> None:
|
||||
@@ -1709,6 +1861,8 @@ def _emit_search_results(data: dict, output_format: str) -> None:
|
||||
click.echo(yaml.safe_dump(data, sort_keys=False))
|
||||
else:
|
||||
click.echo(f"{data['count']} match(es)")
|
||||
if data.get("policy"):
|
||||
_emit_policy_summary(data["policy"])
|
||||
for match in data["matches"]:
|
||||
span = ""
|
||||
if match.get("line_start"):
|
||||
@@ -1720,6 +1874,19 @@ def _emit_search_results(data: dict, output_format: str) -> None:
|
||||
preview = " ".join(str(match.get("text", "")).split())
|
||||
if preview:
|
||||
click.echo(f" {preview[:160]}")
|
||||
for diagnostic in data.get("diagnostics", []):
|
||||
click.echo(f"! [{diagnostic['severity']}] {diagnostic['code']}: {diagnostic['message']}")
|
||||
|
||||
|
||||
def _emit_policy_summary(policy_data: dict) -> None:
|
||||
click.echo(
|
||||
"policy: "
|
||||
f"mode={policy_data.get('mode')} "
|
||||
f"subject={policy_data.get('subject')} "
|
||||
f"allowed={policy_data.get('allowed', 0)} "
|
||||
f"denied={policy_data.get('denied', 0)} "
|
||||
f"redacted={policy_data.get('redacted', 0)}"
|
||||
)
|
||||
|
||||
|
||||
def _emit_workflow_result(data: dict, output_format: str) -> None:
|
||||
|
||||
Reference in New Issue
Block a user