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