# -*- coding: utf-8 -*- """``compositor.audit.log`` — Append-only audit log. Records every compositor event (app launch, terminate, crash, session close) for compliance, debugging, and security forensics. Records CANNOT be modified or deleted via the UI. Only superuser can directly delete via SQL (e.g. for archival). """ from odoo import api, fields, models from odoo.exceptions import UserError class CompositorAuditLog(models.Model): _name = "compositor.audit.log" _description = "1B Space Compositor Audit Log (append-only)" _order = "create_date desc" _rec_name = "create_date" # ─── Identity ──────────────────────────────────────────────────────────── user_id = fields.Many2one( "res.users", string="User", required=True, index=True, ) session_id = fields.Many2one( "compositor.session", string="Session", index=True, ) app_id = fields.Many2one( "compositor.app", string="Application", index=True, ) app_instance_id = fields.Many2one( "compositor.session.app", string="App Instance", index=True, ) # ─── Event ────────────────────────────────────────────────────────────── event_type = fields.Selection( [ ("session_start", "Session Started"), ("session_close", "Session Closed"), ("session_reconnect", "Session Reconnected"), ("session_timeout", "Session Timed Out (SIGHUP)"), ("app_launch", "App Launched"), ("app_terminate", "App Terminated"), ("app_crash", "App Crashed"), ("app_force_kill", "App Force-Killed"), ("jwt_issued", "JWT Issued"), ("jwt_validation_failed", "JWT Validation Failed"), ("proxy_started", "Proxy Started"), ("proxy_crashed", "Proxy Crashed"), ], string="Event Type", required=True, index=True, ) # ─── Detail ───────────────────────────────────────────────────────────── pid = fields.Integer(string="PID") exit_code = fields.Integer(string="Exit Code") signal_received = fields.Char(string="Signal Received") detail = fields.Text(string="Detail") client_ip = fields.Char(string="Client IP") user_agent = fields.Char(string="User Agent") # ─── Lifecycle ────────────────────────────────────────────────────────── create_date = fields.Datetime( string="Timestamp", default=fields.Datetime.now, readonly=True, index=True, ) # ─── Append-only enforcement ──────────────────────────────────────────── def write(self, vals): if not self.env.context.get("compositor_allow_audit_write"): raise UserError( "Audit logs are append-only and cannot be modified.\n\n" "To fix a mistake, create a new corrective log entry with " "context={'compositor_allow_audit_write': True} (admin only)." ) return super().write(vals) def unlink(self): if not self.env.user.has_group("base.group_system"): raise UserError( "Audit logs can only be deleted by system administrators." ) return super().unlink() # ─── Public methods ──────────────────────────────────────────────────── @api.model def log_event(self, event_type, **kwargs): """Convenience method to add a log entry. :param event_type: One of the event_type selection values. :param kwargs: Field values for the new record. :return: The created record. """ defaults = { "user_id": self.env.user.id, "create_date": fields.Datetime.now(), } defaults.update(kwargs) # Bypass append-only check since this is the create path return self.sudo().with_context(compositor_allow_audit_write=True).create({ "event_type": event_type, **defaults, })