Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions cetmix_tower_jet_isolation/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
==========================
Cetmix Tower Jet Isolation
==========================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-cetmix%2Fcetmix--tower-lightgray.png?logo=github
:target: https://github.com/cetmix/cetmix-tower
:alt: cetmix/cetmix-tower

|badge1| |badge2| |badge3|

Adds strict isolation mode for Jets, preventing users from altering command visibility.

**Table of contents**

.. contents::
:local:

Installation
============

To install this module, you need to:

1. Clone cetmix-tower repository.
2. Install this module `cetmix_tower_jet_isolation` into your Odoo instance.

Configuration
=============

To configure this module, you need to:

1. Go to Tower > Jet Templates.
2. Open a Jet Template.
3. Enable 'Isolation Mode' and configure forced applicability and tags.

Usage
=====

When users run a command or a flight plan from an isolated Jet, the wizard will have the applicability options and tag selections locked, enforcing the parameters defined by the administrator in the Template.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/cetmix/cetmix-tower/issues>`_.

Credits
=======

Authors
~~~~~~~

* Cetmix
* Crumges

Maintainers
~~~~~~~~~~~

This module is maintained by the Cetmix team.
2 changes: 2 additions & 0 deletions cetmix_tower_jet_isolation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models, wizards
from .hooks import uninstall_hook
19 changes: 19 additions & 0 deletions cetmix_tower_jet_isolation/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "Cetmix Tower Jet Isolation",
"version": "18.0.1.0.1",
"category": "Tower",
"summary": "Adds strict isolation mode for Jets.",
"author": "Cetmix, Crumges",
"website": "https://tower.cetmix.com",
"depends": [
"cetmix_tower_server",
],
"data": [
"views/cx_tower_jet_template_views.xml",
"views/cx_tower_command_run_wizard_views.xml",
"views/cx_tower_plan_run_wizard_views.xml",
],
"installable": True,
"license": "AGPL-3",
"uninstall_hook": "uninstall_hook",
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
17 changes: 17 additions & 0 deletions cetmix_tower_jet_isolation/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
def uninstall_hook(env):
"""Reset isolation fields on all jet templates when the module is uninstalled.

Odoo does not drop columns when a module is uninstalled, so any templates
that had ``isolation_mode`` enabled would still carry the value ``True``
in their row. When the module is re-installed (or the column is somehow
still read) those stale values would silently reactivate the restriction
logic. Resetting them here ensures a clean state.
"""
env["cx.tower.jet.template"].search([("isolation_mode", "=", True)]).write(
{
"isolation_mode": False,
"forced_applicability": False,
"forced_command_tag_ids": [(5,)],
"forced_plan_tag_ids": [(5,)],
}
)
Comment on lines +10 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find and examine the hooks.py file
find . -name "hooks.py" -path "*cetmix_tower_jet_isolation*" -type f

Repository: cetmix/cetmix-tower

Length of output: 100


🏁 Script executed:

# Also search for the model definition to understand the structure
find . -path "*cetmix_tower_jet_isolation*" -name "*.py" -type f | head -20

Repository: cetmix/cetmix-tower

Length of output: 477


🏁 Script executed:

# Check for model definitions with cx.tower.jet.template
rg "cx\.tower\.jet\.template" --type py -A 5 -B 5

Repository: cetmix/cetmix-tower

Length of output: 50375


🏁 Script executed:

cat -n ./cetmix_tower_jet_isolation/hooks.py

Repository: cetmix/cetmix-tower

Length of output: 937


🏁 Script executed:

cat -n ./cetmix_tower_jet_isolation/models/cx_tower_jet_template.py

Repository: cetmix/cetmix-tower

Length of output: 882


🌐 Web query:

Odoo active_test default behavior models with active field

💡 Result:

In Odoo, models that define an 'active' field (typically BooleanField with default=True) have automatic filtering behavior in ORM methods like search, read_group, etc. The parameter 'active_test' controls whether this filtering is applied. Default behavior: active_test defaults to True when not specified in the context. This means searches and similar operations automatically filter to only include records where active=True (active records), excluding archived/inactive ones (active=False). To include inactive records, explicitly set active_test=False in the context, e.g.: self.with_context(active_test=False).search(domain) or in views: This applies to any model with an 'active' field (or custom _active_name field). Official docs confirm the 'active' field "toggles the global visibility of the record, if active is set to False the record is invisible in most searches and listing." Multiple secondary sources and code examples confirm the default is True, requiring explicit False to bypass filtering.

Citations:


🏁 Script executed:

# Find the parent cx.tower.jet.template model definition
rg "_name = \"cx\.tower\.jet\.template\"" --type py -A 20 -B 2

Repository: cetmix/cetmix-tower

Length of output: 3753


🏁 Script executed:

# Check if the parent model has an active field
rg "active.*Boolean" cetmix_tower_server/models/cx_tower_jet_template.py -A 2 -B 2

Repository: cetmix/cetmix-tower

Length of output: 190


Reset all isolation fields on both active and archived templates.

The current search only updates active templates with isolation_mode=True. Odoo's active_test parameter defaults to True, which automatically excludes archived records from all searches. Additionally, templates with isolation_mode=False but stale forced_* values are not reset at all. To ensure a truly clean state on module reinstall as documented in the hook docstring, clear isolation fields for all templates regardless of state.

🛠️ Proposed fix
-    env["cx.tower.jet.template"].search([("isolation_mode", "=", True)]).write(
+    env["cx.tower.jet.template"].with_context(active_test=False).search([]).write(
         {
             "isolation_mode": False,
             "forced_applicability": False,
             "forced_command_tag_ids": [(5,)],
             "forced_plan_tag_ids": [(5,)],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cetmix_tower_jet_isolation/hooks.py` around lines 10 - 17, The hook only
resets active templates with isolation_mode=True and thus skips archived records
and templates that already have isolation_mode=False but stale forced_* tag
values; change the search call on env["cx.tower.jet.template"] to select all
records (no isolation_mode domain) and pass active_test=False (e.g., search([],
active_test=False)) so archived templates are included, then call write(...)
with the same payload to clear isolation_mode, forced_applicability and
force-clear forced_command_tag_ids/forced_plan_tag_ids for every template.

1 change: 1 addition & 0 deletions cetmix_tower_jet_isolation/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import cx_tower_jet_template
22 changes: 22 additions & 0 deletions cetmix_tower_jet_isolation/models/cx_tower_jet_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from odoo import fields, models


class CxTowerJetTemplate(models.Model):
_inherit = "cx.tower.jet.template"

isolation_mode = fields.Boolean(
help="Prevents changing applicability or tags when running commands."
)
forced_applicability = fields.Selection(
[("this", "For selected server(s)"), ("shared", "Non server restricted")]
)

forced_command_tag_ids = fields.Many2many(
comodel_name="cx.tower.tag",
relation="cx_tower_template_forced_command_tag_rel",
)

forced_plan_tag_ids = fields.Many2many(
comodel_name="cx.tower.tag",
relation="cx_tower_template_forced_plan_tag_rel",
)
Comment on lines +14 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Specify column1/column2 for the Many2many relations and follow repo naming.

Other cx.tower.tag Many2many relations in cetmix_tower_server/models/cx_tower_tag.py declare explicit column1/column2 (e.g., tag_id / <target>_id). Omitting them lets Odoo auto-generate columns from field names, which is fragile across renames. Also, the relation table name cx_tower_template_forced_command_tag_rel elides the "jet" segment — consider cx_tower_jet_template_forced_command_tag_rel for clarity and to avoid collisions with other template models.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cetmix_tower_jet_isolation/models/cx_tower_jet_template.py` around lines 15 -
25, Update the Many2many definitions for forced_command_tag_ids and
forced_plan_tag_ids to explicitly declare column1 and column2 and rename the
relation tables to include "jet" to match repo conventions; specifically, for
the field forced_command_tag_ids (comodel_name='cx.tower.tag', relation
currently 'cx_tower_template_forced_command_tag_rel') add
column1='jet_template_id' and column2='tag_id' and rename the relation to
'cx_tower_jet_template_forced_command_tag_rel', and for forced_plan_tag_ids add
column1='jet_template_id' and column2='tag_id' and rename its relation to
'cx_tower_jet_template_forced_plan_tag_rel' so the DB columns and table name
match other cx.tower.tag Many2manys and avoid auto-generated fragile names.

3 changes: 3 additions & 0 deletions cetmix_tower_jet_isolation/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="cx_tower_command_run_wizard_view_form_isolation" model="ir.ui.view">
<field name="name">cx.tower.command.run.wizard.view.form.isolation</field>
<field name="model">cx.tower.command.run.wizard</field>
<field
name="inherit_id"
ref="cetmix_tower_server.cx_tower_command_run_wizard_view_form"
/>
<field name="arch" type="xml">
<xpath expr="//field[@name='tag_ids']" position="before">
<field name="is_restricted_context" invisible="1" />
</xpath>

<xpath expr="//label[@for='applicability']" position="attributes">
<attribute name="invisible">is_restricted_context or result</attribute>
</xpath>

<xpath expr="//field[@name='applicability']" position="attributes">
<attribute name="readonly">is_restricted_context</attribute>
<attribute name="force_save">1</attribute>
</xpath>
Comment on lines +19 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Overriding readonly drops existing show_servers condition on applicability.

The parent view sets readonly="show_servers" on applicability. Replacing the attribute with readonly="is_isolated_context" silently removes the original condition, so applicability will become editable again in contexts where it previously wasn't (e.g., after servers are computed). Combine both conditions.

🔧 Proposed fix
             <xpath expr="//field[`@name`='applicability']" position="attributes">
-                <attribute name="readonly">is_isolated_context</attribute>
+                <attribute name="readonly">is_isolated_context or show_servers</attribute>
                 <attribute name="force_save">1</attribute>
             </xpath>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<xpath expr="//field[@name='applicability']" position="attributes">
<attribute name="readonly">is_isolated_context</attribute>
<attribute name="force_save">1</attribute>
</xpath>
<xpath expr="//field[`@name`='applicability']" position="attributes">
<attribute name="readonly">is_isolated_context or show_servers</attribute>
<attribute name="force_save">1</attribute>
</xpath>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cetmix_tower_jet_isolation/views/cx_tower_command_run_wizard_views.xml`
around lines 15 - 18, The override of the applicability field replaced the
original readonly condition and removed the parent condition show_servers;
update the readonly attribute on the applicability field (the xpath targeting
//field[`@name`='applicability']) to combine both conditions instead of replacing
them—e.g. set readonly to an expression that ORs the two flags like
"show_servers or is_isolated_context" so the field remains readonly when either
condition applies, and keep the existing force_save="1" attribute.


<xpath expr="//field[@name='tag_ids']" position="attributes">
<attribute name="groups">cetmix_tower_server.group_manager</attribute>
<attribute name="invisible">is_restricted_context or result</attribute>
<attribute name="force_save">1</attribute>
</xpath>
Comment on lines +24 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify static group narrowing on tag_ids versus dynamic restriction logic.
rg -n -C4 'name="tag_ids"|is_restricted_context|manager_ids|has_group\("cetmix_tower_server.group_manager"\)' \
  cetmix_tower_jet_isolation/views/cx_tower_command_run_wizard_views.xml \
  cetmix_tower_jet_isolation/wizards/cx_tower_command_run_wizard.py

Repository: cetmix/cetmix-tower

Length of output: 7612


Remove static group restriction on tag_ids field.

Line 25 unnecessarily restricts tag_ids visibility to global managers only using groups="cetmix_tower_server.group_manager". This contradicts the dynamic is_restricted_context logic, which allows jet managers (non-global) to see the field when managing their own jets. The existing invisible="is_restricted_context or result" attribute already provides the correct visibility control.

Proposed fix
             <xpath expr="//field[`@name`='tag_ids']" position="attributes">
-                <attribute name="groups">cetmix_tower_server.group_manager</attribute>
                 <attribute name="invisible">is_restricted_context or result</attribute>
                 <attribute name="force_save">1</attribute>
             </xpath>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<xpath expr="//field[@name='tag_ids']" position="attributes">
<attribute name="groups">cetmix_tower_server.group_manager</attribute>
<attribute name="invisible">is_restricted_context or result</attribute>
<attribute name="force_save">1</attribute>
</xpath>
<xpath expr="//field[`@name`='tag_ids']" position="attributes">
<attribute name="invisible">is_restricted_context or result</attribute>
<attribute name="force_save">1</attribute>
</xpath>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cetmix_tower_jet_isolation/views/cx_tower_command_run_wizard_views.xml`
around lines 24 - 28, Remove the static groups restriction on the tag_ids field
by deleting the attribute line that sets
groups="cetmix_tower_server.group_manager" inside the xpath targeting
//field[`@name`='tag_ids']; keep the existing invisible="is_restricted_context or
result" and force_save attributes so visibility remains dynamically controlled
by is_restricted_context and result logic (i.e., remove the <attribute
name="groups"> entry in the xpath for tag_ids).


<xpath expr="//field[@name='tag_ids']" position="after">
<field
name="isolated_tag_ids"
widget="many2many_tags"
invisible="not is_restricted_context or result"
/>
</xpath>

<xpath expr="//notebook" position="attributes">
<attribute name="groups" />
</xpath>
<xpath expr="//page[@name='code']" position="attributes">
<attribute name="invisible">is_restricted_context</attribute>
</xpath>
<xpath expr="//field[@name='rendered_code']" position="attributes">
<attribute name="groups" />
<attribute name="readonly">1</attribute>
</xpath>
Comment thread
marionumza marked this conversation as resolved.
<xpath
expr="//field[@name='custom_variable_value_ids']"
position="attributes"
>
<attribute
name="readonly"
>not have_access_to_server or is_restricted_context</attribute>
</xpath>
</field>
</record>
</odoo>
33 changes: 33 additions & 0 deletions cetmix_tower_jet_isolation/views/cx_tower_jet_template_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="cx_tower_jet_template_view_form_isolation" model="ir.ui.view">
<field name="name">cx.tower.jet.template.view.form.isolation</field>
<field name="model">cx.tower.jet.template</field>
<field
name="inherit_id"
ref="cetmix_tower_server.cx_tower_jet_template_view_form"
/>
<field name="arch" type="xml">
<xpath expr="//group[@name='general']" position="after">
<group name="isolation" string="Execution Security (Isolation Mode)">
<field name="isolation_mode" widget="boolean_toggle" />
<field
name="forced_applicability"
invisible="not isolation_mode"
required="isolation_mode"
/>
<field
name="forced_command_tag_ids"
widget="many2many_tags"
invisible="not isolation_mode"
/>
<field
name="forced_plan_tag_ids"
widget="many2many_tags"
invisible="not isolation_mode"
/>
</group>
</xpath>
</field>
</record>
</odoo>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="cx_tower_plan_run_wizard_view_form_isolation" model="ir.ui.view">
<field name="name">cx.tower.plan.run.wizard.view.form.isolation</field>
<field name="model">cx.tower.plan.run.wizard</field>
<field
name="inherit_id"
ref="cetmix_tower_server.cx_tower_plan_run_wizard_view_form"
/>
<field name="arch" type="xml">
<xpath expr="//field[@name='tag_ids']" position="before">
<field name="is_restricted_context" invisible="1" />
</xpath>

<xpath expr="//label[@for='applicability']" position="attributes">
<attribute name="invisible">is_restricted_context</attribute>
</xpath>

<xpath expr="//field[@name='applicability']" position="attributes">
<attribute name="readonly">is_restricted_context</attribute>
<attribute name="force_save">1</attribute>
</xpath>
Comment on lines +19 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f 'cx_tower_plan_run_wizard_view.xml' | xargs -I{} rg -n -C2 "name=['\"]applicability['\"]" {}

Repository: cetmix/cetmix-tower

Length of output: 299


🏁 Script executed:

# Find the parent view by examining the file and locating the parent reference
fd -t f '*.xml' -p 'cetmix_tower_jet_isolation/views/*' | head -20

Repository: cetmix/cetmix-tower

Length of output: 184


🏁 Script executed:

# Search for the parent view name referenced in the file being reviewed
rg -A5 -B5 "cx_tower_plan_run_wizard_view" cetmix_tower_jet_isolation/views/cx_tower_plan_run_wizard_views.xml | head -40

Repository: cetmix/cetmix-tower

Length of output: 658


🏁 Script executed:

# Look for any parent view definition files that might contain applicability field
fd -t f '*.xml' cetmix_tower_jet_isolation/views/ | xargs grep -l "applicability" | head -10

Repository: cetmix/cetmix-tower

Length of output: 347


🏁 Script executed:

# List all XML files to understand the structure
find . -name "*.xml" -type f | grep -E "(cetmix_tower_server|cetmix_tower_jet)" | head -20

Repository: cetmix/cetmix-tower

Length of output: 1292


🏁 Script executed:

# Search for the parent view definition across the entire repo
rg -l "cx_tower_plan_run_wizard_view_form" --type xml

Repository: cetmix/cetmix-tower

Length of output: 192


🏁 Script executed:

# Search for applicability field with readonly attribute
rg "readonly.*applicability|applicability.*readonly" --type xml -A2 -B2

Repository: cetmix/cetmix-tower

Length of output: 45


🏁 Script executed:

# Read the parent view file to find applicability field definition
cat -n cetmix_tower_server/wizards/cx_tower_plan_run_wizard_view.xml | grep -A10 -B10 "applicability"

Repository: cetmix/cetmix-tower

Length of output: 1462


Combine readonly conditions instead of replacing: The parent view already sets readonly="show_servers" on the applicability field (line 31 of cetmix_tower_server/wizards/cx_tower_plan_run_wizard_view.xml). The xpath with position="attributes" will override this, causing the field to ignore the show_servers guard. Change the attribute to readonly="show_servers or is_isolated_context" to preserve both conditions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cetmix_tower_jet_isolation/views/cx_tower_plan_run_wizard_views.xml` around
lines 15 - 18, The xpath that sets attributes on the field applicability
currently replaces the parent readonly condition; update the readonly attribute
to combine both guards by setting readonly="show_servers or is_isolated_context"
(keep the existing force_save attribute unchanged) so the field honors both the
parent show_servers and the local is_isolated_context conditions.


<xpath expr="//field[@name='tag_ids']" position="attributes">
<attribute name="invisible">is_restricted_context</attribute>
<attribute name="force_save">1</attribute>
</xpath>

<xpath expr="//field[@name='tag_ids']" position="after">
<field
name="isolated_tag_ids"
widget="many2many_tags"
invisible="not is_restricted_context"
/>
</xpath>

<xpath
expr="//field[@name='custom_variable_value_ids']"
position="attributes"
>
<attribute name="readonly">is_restricted_context</attribute>
</xpath>
</field>
</record>
</odoo>
2 changes: 2 additions & 0 deletions cetmix_tower_jet_isolation/wizards/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import cx_tower_command_run_wizard
from . import cx_tower_plan_run_wizard
55 changes: 55 additions & 0 deletions cetmix_tower_jet_isolation/wizards/cx_tower_command_run_wizard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from odoo import api, fields, models


class CxTowerCommandRunWizardFilter(models.TransientModel):
_inherit = "cx.tower.command.run.wizard"

is_restricted_context = fields.Boolean(compute="_compute_is_restricted_context")
isolated_tag_ids = fields.Many2many(
comodel_name="cx.tower.tag",
compute="_compute_isolated_tag_ids",
string="Forced Tags",
)

@api.depends("jet_ids")
def _compute_is_restricted_context(self):
is_global_manager = self.env.user.has_group("cetmix_tower_server.group_manager")
for record in self:
jets = record.jet_ids or self.env["cx.tower.jet"].browse(
self.env.context.get("default_jet_ids", [])
)

is_isolated = bool(
jets and any(j.jet_template_id.isolation_mode for j in jets)
)

if is_global_manager:
is_manager = True
elif jets and all(self.env.user in j.manager_ids for j in jets):
is_manager = True
else:
is_manager = False

record.is_restricted_context = is_isolated and not is_manager

@api.depends("tag_ids")
def _compute_isolated_tag_ids(self):
for record in self:
record.isolated_tag_ids = record.tag_ids

@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
if "default_jet_ids" in self.env.context:
jet_ids = self.env["cx.tower.jet"].browse(
self.env.context["default_jet_ids"]
)
if jet_ids:
template = jet_ids[0].jet_template_id

if template.isolation_mode:
if template.forced_applicability:
res["applicability"] = template.forced_applicability
if template.forced_command_tag_ids:
res["tag_ids"] = [(6, 0, template.forced_command_tag_ids.ids)]
return res
Comment on lines +40 to +55
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Inconsistency between _compute_is_isolated_context and default_get — isolation can be bypassed.

_compute_is_isolated_context activates when any selected jet's template has isolation_mode enabled (Line 11), but default_get only inspects jet_ids[0].jet_template_id (Line 22). If the first selected jet's template has no isolation, but a later one does, the wizard enters isolated (read-only) context without the forced applicability/tag_ids being applied — defeating the isolation guarantees described in the PR objectives.

Iterate over all jets and apply the strictest forced values, or otherwise define a deterministic selection rule (e.g., the first isolated template).

🛠️ Proposed fix
-        if 'default_jet_ids' in self.env.context:
-            jet_ids = self.env['cx.tower.jet'].browse(self.env.context['default_jet_ids'])
-            if jet_ids:
-                template = jet_ids[0].jet_template_id
-                
-                if template.isolation_mode:
-                    if template.forced_applicability:
-                        res['applicability'] = template.forced_applicability
-                    if template.forced_command_tag_ids:
-                        res['tag_ids'] = [(6, 0, template.forced_command_tag_ids.ids)]
+        jet_ids_ctx = self.env.context.get('default_jet_ids')
+        if jet_ids_ctx:
+            jets = self.env['cx.tower.jet'].browse(jet_ids_ctx)
+            isolated_templates = jets.jet_template_id.filtered('isolation_mode')
+            if isolated_templates:
+                template = isolated_templates[0]
+                if template.forced_applicability:
+                    res['applicability'] = template.forced_applicability
+                forced_tags = isolated_templates.mapped('forced_command_tag_ids')
+                if forced_tags:
+                    res['tag_ids'] = [(6, 0, forced_tags.ids)]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cetmix_tower_jet_isolation/wizards/cx_tower_command_run_wizard.py` around
lines 16 - 29, default_get currently only inspects jet_ids[0].jet_template_id
which can miss isolation info used by _compute_is_isolated_context; update
default_get to iterate all jets in jet_ids, collect templates where
template.isolation_mode is True, then apply a deterministic rule (e.g., pick the
first isolated template) or merge strictest values: set res['applicability']
from the chosen template.forced_applicability (if present) and res['tag_ids'] to
the union of all chosen templates' forced_command_tag_ids.ids (if any) so
forced_applicability and forced_command_tag_ids are applied whenever any
selected jet enforces isolation; reference variables/functions: default_get,
jet_ids, template, template.isolation_mode, template.forced_applicability,
template.forced_command_tag_ids, res['applicability'], res['tag_ids'].

55 changes: 55 additions & 0 deletions cetmix_tower_jet_isolation/wizards/cx_tower_plan_run_wizard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from odoo import api, fields, models


class CxTowerPlanRunWizardFilter(models.TransientModel):
_inherit = "cx.tower.plan.run.wizard"

is_restricted_context = fields.Boolean(compute="_compute_is_restricted_context")
isolated_tag_ids = fields.Many2many(
comodel_name="cx.tower.tag",
compute="_compute_isolated_tag_ids",
string="Forced Tags",
)

@api.depends("jet_ids")
def _compute_is_restricted_context(self):
is_global_manager = self.env.user.has_group("cetmix_tower_server.group_manager")
for record in self:
jets = record.jet_ids or self.env["cx.tower.jet"].browse(
self.env.context.get("default_jet_ids", [])
)

is_isolated = bool(
jets and any(j.jet_template_id.isolation_mode for j in jets)
)

if is_global_manager:
is_manager = True
elif jets and all(self.env.user in j.manager_ids for j in jets):
is_manager = True
else:
is_manager = False

record.is_restricted_context = is_isolated and not is_manager

@api.depends("tag_ids")
def _compute_isolated_tag_ids(self):
for record in self:
record.isolated_tag_ids = record.tag_ids

@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
if "default_jet_ids" in self.env.context:
jet_ids = self.env["cx.tower.jet"].browse(
self.env.context["default_jet_ids"]
)
if jet_ids:
template = jet_ids[0].jet_template_id

if template.isolation_mode:
if template.forced_applicability:
res["applicability"] = template.forced_applicability
if template.forced_plan_tag_ids:
res["tag_ids"] = [(6, 0, template.forced_plan_tag_ids.ids)]
return res
Comment on lines +40 to +55
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Server-side enforcement is missing — isolation is bypassable.

default_get only prefills applicability/tag_ids on form open; the view's readonly + force_save="1" is a client-side restriction. A user can still submit arbitrary values via RPC (e.g., create/write/direct button call) and bypass the isolation filter entirely, defeating the stated security goal of this module.

Enforce the isolation constraints on the server side by overriding create/write (or the action that launches the plan) to re-apply forced_applicability and forced_plan_tag_ids whenever is_isolated_context is true, or validate them in a @api.constrains. The same concern applies symmetrically in cetmix_tower_jet_isolation/wizards/cx_tower_command_run_wizard.py.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cetmix_tower_jet_isolation/wizards/cx_tower_plan_run_wizard.py` around lines
16 - 29, default_get currently only pre-fills applicability/tag_ids but does not
enforce them server-side, so override create and write on the wizard model (the
same pattern should be applied to cx_tower_command_run_wizard.py) to enforce the
template's forced values: when is_isolated_context is true (use the wizard
model's field), fetch the related cx.tower.jet -> jet_template_id and, if
template.isolation_mode, set/replace record values for applicability with
template.forced_applicability and tag_ids with template.forced_plan_tag_ids.ids
(or refuse the operation) before saving; alternatively add an `@api.constrains`
that validates incoming applicability/tag_ids against template.forced_* and
raises an error if they diverge.

Loading