Includes:
- compositor.app, compositor.session, compositor.session.app, compositor.audit.log models
- /web/compositor/{token,apps,launch,audit} HTTP controllers with JWT (HS256)
- CompositorService OWL service (reusable via useService('compositor'))
- CompositorCanvas OWL component (reusable canvas wrapper)
- Security: compositor_group_user, compositor_group_manager groups + ir.access.csv rules
- 5 demo apps pre-loaded (gnome-calculator, xterm, gtk4-demo, glxgears, featherpad)
- Audit log capturing JWT issuance + app launch events
- Stub mode: launching apps returns a stub SurfaceHandle when proxy isn't connected
- Tested against Odoo 20 + PostgreSQL 17
Author: 1bspace.com
Parent company: 1B Global
License: Proprietary (1B Space internal use)
116 lines
4.6 KiB
Python
116 lines
4.6 KiB
Python
# -*- 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,
|
|
})
|