Skip to content

Commit 762e2b4

Browse files
author
Guewen Baconnier
committed
Add safety to prevent crafting queue.job records
As we can delay a job on any method, and the queue.job model is accessible from RPC (as any model), prevent to: * create a queue.job using RPC * write on protected fields (e.g. method name) using RPC Admittedly, the risk is low since users need have Queue Job Manager access to create/write on jobs, but it would allow these users to call internal methods. The check is done using a context key that must be equal to a sentinel object, which is impossible to pass through RPC.
1 parent 417a466 commit 762e2b4

4 files changed

Lines changed: 83 additions & 8 deletions

File tree

queue_job/job.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -553,9 +553,14 @@ def store(self):
553553
if self.identity_key:
554554
vals["identity_key"] = self.identity_key
555555

556+
job_model = self.env["queue.job"]
557+
# The sentinel is used to prevent edition sensitive fields (such as
558+
# method_name) from RPC methods.
559+
edit_sentinel = job_model.EDIT_SENTINEL
560+
556561
db_record = self.db_record()
557562
if db_record:
558-
db_record.write(vals)
563+
db_record.with_context(_job_edit_sentinel=edit_sentinel).write(vals)
559564
else:
560565
date_created = self.date_created
561566
# The following values must never be modified after the
@@ -577,7 +582,7 @@ def store(self):
577582
if self.channel:
578583
vals.update({"channel": self.channel})
579584

580-
self.env[self.job_model_name].sudo().create(vals)
585+
job_model.with_context(_job_edit_sentinel=edit_sentinel).sudo().create(vals)
581586

582587
def db_record(self):
583588
return self.db_record_from_uuid(self.env, self.uuid)

queue_job/models/queue_job.py

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,22 @@ class QueueJob(models.Model):
3232
_removal_interval = 30 # days
3333
_default_related_action = "related_action_open_record"
3434

35+
# This must be passed in a context key "_job_edit_sentinel" to write on
36+
# protected fields. It protects against crafting "queue.job" records from
37+
# RPC (e.g. on internal methods). When ``with_delay`` is used, the sentinel
38+
# is set.
39+
EDIT_SENTINEL = object()
40+
_protected_fields = (
41+
"uuid",
42+
"name",
43+
"date_created",
44+
"model_name",
45+
"method_name",
46+
"record_ids",
47+
"args",
48+
"kwargs",
49+
)
50+
3551
uuid = fields.Char(string="UUID", readonly=True, index=True, required=True)
3652
user_id = fields.Many2one(comodel_name="res.users", string="User ID", required=True)
3753
company_id = fields.Many2one(
@@ -126,6 +142,33 @@ def _compute_func_string(self):
126142
all_args = ", ".join(args + kwargs)
127143
record.func_string = "{}.{}({})".format(model, record.method_name, all_args)
128144

145+
@api.model_create_multi
146+
def create(self, vals_list):
147+
if self.env.context.get("_job_edit_sentinel") is not self.EDIT_SENTINEL:
148+
# Prevent to create a queue.job record "raw" from RPC.
149+
# ``with_delay()`` must be used.
150+
raise exceptions.AccessError(
151+
_("Queue jobs must created by calling 'with_delay()'.")
152+
)
153+
return super().create(vals_list)
154+
155+
def write(self, vals):
156+
if self.env.context.get("_job_edit_sentinel") is not self.EDIT_SENTINEL:
157+
write_on_protected_fields = [
158+
fieldname in self._protected_fields for fieldname in vals
159+
]
160+
if write_on_protected_fields:
161+
raise exceptions.AccessError(
162+
_("Not allowed to change field(s): {}").format(
163+
write_on_protected_fields
164+
)
165+
)
166+
167+
if vals.get("state") == "failed":
168+
self._message_post_on_failure()
169+
170+
return super().write(vals)
171+
129172
def open_related_action(self):
130173
"""Open the related action associated to the job"""
131174
self.ensure_one()
@@ -171,12 +214,6 @@ def _message_post_on_failure(self):
171214
if msg:
172215
record.message_post(body=msg, subtype="queue_job.mt_job_failed")
173216

174-
def write(self, vals):
175-
res = super(QueueJob, self).write(vals)
176-
if vals.get("state") == "failed":
177-
self._message_post_on_failure()
178-
return res
179-
180217
def _subscribe_users_domain(self):
181218
"""Subscribe all users having the 'Queue Job Manager' group"""
182219
group = self.env.ref("queue_job.group_queue_job_manager")

queue_job/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
from . import test_runner_runner
33
from . import test_json_field
44
from . import test_model_job_channel
5+
from . import test_queue_job_protected_write
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# copyright 2020 Camptocamp
2+
# license lgpl-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
3+
4+
from odoo import exceptions
5+
from odoo.tests import common
6+
7+
8+
class TestJobWriteProtected(common.SavepointCase):
9+
10+
def test_create_error(self):
11+
with self.assertRaises(exceptions.AccessError):
12+
self.env["queue.job"].create({
13+
"uuid": "test",
14+
"model_name": "res.partner",
15+
"method_name": "write"
16+
})
17+
18+
def test_write_protected_field_error(self):
19+
job_ = self.env["res.partner"].with_delay().create({
20+
"name": "test",
21+
})
22+
db_job = job_.db_record()
23+
with self.assertRaises(exceptions.AccessError):
24+
db_job.method_name = "unlink"
25+
26+
def test_write_allow_no_protected_field_error(self):
27+
job_ = self.env["res.partner"].with_delay().create({
28+
"name": "test",
29+
})
30+
db_job = job_.db_record()
31+
with self.assertRaises(exceptions.AccessError):
32+
db_job.priority = 30

0 commit comments

Comments
 (0)