# retoor <retoor@molodetz.nl>
import json
import logging
from typing import Any, Optional
from devplacepy.utils import strip_html, generate_uid, client_ip
from devplacepy.services.audit import store
from devplacepy.services.audit.categories import category_for
from devplacepy.services.background import background
logger = logging.getLogger(__name__)
_UNSET = object()
SUMMARY_LIMIT = 140
def link(
relation: str,
object_type: str,
object_uid: Optional[str],
object_label: Optional[str] = None,
) -> dict:
return {
"relation": relation,
"object_type": object_type,
"object_uid": object_uid,
"object_label": object_label,
}
def actor(uid: Optional[str], label: Optional[str] = None) -> dict:
return link("actor", "user", uid, label)
def target(object_type: str, uid: Optional[str], label: Optional[str] = None) -> dict:
return link("target", object_type, uid, label)
def parent(object_type: str, uid: Optional[str], label: Optional[str] = None) -> dict:
return link("parent", object_type, uid, label)
def author(uid: Optional[str], label: Optional[str] = None) -> dict:
return link("author", "user", uid, label)
def recipient(uid: Optional[str], label: Optional[str] = None) -> dict:
return link("recipient", "user", uid, label)
def mention(uid: Optional[str], label: Optional[str] = None) -> dict:
return link("mention", "user", uid, label)
def project(uid: Optional[str], label: Optional[str] = None) -> dict:
return link("project", "project", uid, label)
def attachment_link(uid: Optional[str], label: Optional[str] = None) -> dict:
return link("attachment", "attachment", uid, label)
def source(object_type: str, uid: Optional[str], label: Optional[str] = None) -> dict:
return link("source", object_type, uid, label)
def destination(object_type: str, uid: Optional[str], label: Optional[str] = None) -> dict:
return link("destination", object_type, uid, label)
def schedule(uid: Optional[str], label: Optional[str] = None) -> dict:
return link("schedule", "schedule", uid, label)
def instance(uid: Optional[str], label: Optional[str] = None) -> dict:
return link("instance", "instance", uid, label)
def setting(key: Optional[str], label: Optional[str] = None) -> dict:
return link("setting", "setting", key, label or key)
def service_link(name: Optional[str], label: Optional[str] = None) -> dict:
return link("target", "service", name, label or name)
def job(uid: Optional[str], label: Optional[str] = None) -> dict:
return link("job", "job", uid, label)
def poll(uid: Optional[str], label: Optional[str] = None) -> dict:
return link("poll", "poll", uid, label)
def option(uid: Optional[str], label: Optional[str] = None) -> dict:
return link("option", "poll_option", uid, label)
def task(uid: Optional[str], label: Optional[str] = None) -> dict:
return link("target", "task", uid, label)
def _actor_from(user: Optional[dict]) -> dict:
if not user:
return {
"actor_kind": "guest",
"actor_uid": None,
"actor_username": None,
"actor_role": "guest",
}
return {
"actor_kind": "user",
"actor_uid": user.get("uid"),
"actor_username": user.get("username"),
"actor_role": "admin" if user.get("role") == "Admin" else "member",
}
def _request_fields(request) -> dict:
fields: dict = {
"request_method": None,
"request_path": None,
"actor_ip": None,
"user_agent": None,
"devii": False,
}
if request is None:
return fields
try:
fields["request_method"] = getattr(request, "method", None)
url = getattr(request, "url", None)
fields["request_path"] = url.path if url is not None else None
fields["actor_ip"] = client_ip(request, default=None)
headers = getattr(request, "headers", None)
if headers is not None:
fields["user_agent"] = headers.get("user-agent")
fields["devii"] = headers.get("x-devii-agent") == "1"
except Exception as exc:
logger.debug("audit request field read failed: %s", exc)
return fields
def _sanitize_summary(text: Optional[str], limit: int = SUMMARY_LIMIT) -> Optional[str]:
if not text:
return None
cleaned = strip_html(str(text))
if not cleaned:
return None
if len(cleaned) > limit:
return cleaned[: limit - 3].rstrip() + "..."
return cleaned
def _coerce_scalar(value: Any) -> Optional[str]:
if value is None:
return None
if isinstance(value, bool):
return "1" if value else "0"
return str(value)
def _persist(row: dict, links: list[dict]) -> None:
audit_uid = store.insert_event(row)
store.insert_links(audit_uid, links)
def _write(
*,
event_key: str,
base_actor: dict,
request_fields: dict,
target_type: Optional[str],
target_uid: Optional[str],
target_label: Optional[str],
old_value: Any,
new_value: Any,
summary: Optional[str],
metadata: Optional[dict],
result: str,
origin: Optional[str],
via_agent: Optional[int],
links: Optional[list[dict]],
category: Optional[str],
) -> Optional[str]:
resolved_origin = origin
if resolved_origin is None:
resolved_origin = "devii" if request_fields.get("devii") else "web"
resolved_via = via_agent
if resolved_via is None:
resolved_via = 1 if request_fields.get("devii") else 0
audit_uid = generate_uid()
row = {
"uid": audit_uid,
"created_at": store.now(),
"event_key": event_key,
"category": category or category_for(event_key),
"actor_kind": base_actor.get("actor_kind"),
"actor_uid": base_actor.get("actor_uid"),
"actor_username": base_actor.get("actor_username"),
"actor_role": base_actor.get("actor_role"),
"origin": resolved_origin,
"via_agent": int(resolved_via),
"request_method": request_fields.get("request_method"),
"request_path": request_fields.get("request_path"),
"actor_ip": request_fields.get("actor_ip"),
"user_agent": request_fields.get("user_agent"),
"target_type": target_type,
"target_uid": target_uid,
"target_label": _sanitize_summary(target_label, 200),
"old_value": _coerce_scalar(old_value),
"new_value": _coerce_scalar(new_value),
"summary": _sanitize_summary(summary),
"metadata": json.dumps(metadata) if metadata else None,
"result": result,
}
all_links = list(links or [])
if base_actor.get("actor_uid") and not any(
item.get("relation") == "actor" for item in all_links
):
all_links.append(
actor(base_actor["actor_uid"], base_actor.get("actor_username"))
)
background.submit(_persist, row, all_links)
logger.debug(
"audit %s by %s/%s result=%s origin=%s",
event_key,
row["actor_kind"],
row["actor_username"],
result,
resolved_origin,
)
return audit_uid
def record(
request,
event_key: str,
*,
user=_UNSET,
actor_kind: Optional[str] = None,
target_type: Optional[str] = None,
target_uid: Optional[str] = None,
target_label: Optional[str] = None,
old_value: Any = None,
new_value: Any = None,
summary: Optional[str] = None,
metadata: Optional[dict] = None,
result: str = "success",
origin: Optional[str] = None,
via_agent: Optional[int] = None,
links: Optional[list[dict]] = None,
category: Optional[str] = None,
) -> Optional[str]:
try:
if user is _UNSET:
from devplacepy.utils import get_current_user
try:
user = get_current_user(request)
except Exception:
user = None
base_actor = _actor_from(user)
if actor_kind:
base_actor["actor_kind"] = actor_kind
return _write(
event_key=event_key,
base_actor=base_actor,
request_fields=_request_fields(request),
target_type=target_type,
target_uid=target_uid,
target_label=target_label,
old_value=old_value,
new_value=new_value,
summary=summary,
metadata=metadata,
result=result,
origin=origin,
via_agent=via_agent,
links=links,
category=category,
)
except Exception as exc:
logger.warning("audit.record failed for %s: %s", event_key, exc)
return None
def record_system(
event_key: str,
*,
actor_kind: str = "system",
actor_uid: Optional[str] = None,
actor_username: Optional[str] = None,
actor_role: str = "system",
target_type: Optional[str] = None,
target_uid: Optional[str] = None,
target_label: Optional[str] = None,
old_value: Any = None,
new_value: Any = None,
summary: Optional[str] = None,
metadata: Optional[dict] = None,
result: str = "success",
origin: str = "service",
via_agent: int = 0,
links: Optional[list[dict]] = None,
category: Optional[str] = None,
) -> Optional[str]:
try:
base_actor = {
"actor_kind": actor_kind,
"actor_uid": actor_uid,
"actor_username": actor_username,
"actor_role": actor_role,
}
return _write(
event_key=event_key,
base_actor=base_actor,
request_fields={},
target_type=target_type,
target_uid=target_uid,
target_label=target_label,
old_value=old_value,
new_value=new_value,
summary=summary,
metadata=metadata,
result=result,
origin=origin,
via_agent=via_agent,
links=links,
category=category,
)
except Exception as exc:
logger.warning("audit.record_system failed for %s: %s", event_key, exc)
return None