|
# 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
|