Skip to content

[FIX] queue_job: prevent conflict w/ TestOverrides:test_creates#802

Merged
OCA-git-bot merged 1 commit intoOCA:18.0from
amh-mw:18.0-private_create
Jul 10, 2025
Merged

[FIX] queue_job: prevent conflict w/ TestOverrides:test_creates#802
OCA-git-bot merged 1 commit intoOCA:18.0from
amh-mw:18.0-private_create

Conversation

@amh-mw
Copy link
Copy Markdown
Member

@amh-mw amh-mw commented Jul 7, 2025

This prevents TestOverrides.test_creates from failing in the Odoo base module due to the sentinel protections taking effect even for local create invocations.

Fixes #727

This prevents TestOverrides.test_creates from failing in the Odoo
`base` module due to sentinel protections taking effect even for
local create invocations.
@OCA-git-bot
Copy link
Copy Markdown
Contributor

Hi @guewen,
some modules you are maintaining are being modified, check this out!

@amh-mw
Copy link
Copy Markdown
Member Author

amh-mw commented Jul 7, 2025

Proposal: use https://github.com/odoo/odoo/blob/18.0/odoo/api.py#L446 and get rid of the sentinel. As an alternative we could use rpc_helper but that would add another dependency.

@simahawk I liked your idea and went ahead and implemented it. Tests are green and I will start running this change in my local development environment this week.

@amh-mw
Copy link
Copy Markdown
Member Author

amh-mw commented Jul 7, 2025

@Kimkhoi3010 Would you be willing to review this pull request, since it mirrors your own?

Copy link
Copy Markdown
Member

@guewen guewen left a comment

Choose a reason for hiding this comment

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

TIL @api.private, nice.
Thanks!

Copy link
Copy Markdown

@Kimkhoi3010 Kimkhoi3010 left a comment

Choose a reason for hiding this comment

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

LGTM, thanks

@guewen
Copy link
Copy Markdown
Member

guewen commented Jul 10, 2025

/ocabot merge patch

@OCA-git-bot
Copy link
Copy Markdown
Contributor

On my way to merge this fine PR!
Prepared branch 18.0-ocabot-merge-pr-802-by-guewen-bump-patch, awaiting test results.

@OCA-git-bot OCA-git-bot merged commit d22d362 into OCA:18.0 Jul 10, 2025
7 checks passed
@OCA-git-bot
Copy link
Copy Markdown
Contributor

Congratulations, your PR was merged at 5b7cedd. Thanks a lot for contributing to OCA. ❤️

@amh-mw amh-mw deleted the 18.0-private_create branch July 10, 2025 11:42
@simahawk
Copy link
Copy Markdown
Contributor

That's cool! Question: what about the write method? Shouldn't we remove the sentinel there too? 🤔

@amh-mw
Copy link
Copy Markdown
Member Author

amh-mw commented Jul 11, 2025

That's cool! Question: what about the write method? Shouldn't we remove the sentinel there too? 🤔

I did consider it during implementation, but write wasn't breaking any core Odoo tests and it seemed to me that there could be scenarios that would fail if write were similarly made private, i.e. changing job state from the user interface. Given that I am generally an idiot in a hurry, I implemented only create as I was confident in its correctness and then got back to all the other work I need to do. Sorry!

@simahawk
Copy link
Copy Markdown
Contributor

Given that I am generally an idiot in a hurry, I implemented only create as I was confident in its correctness and then got back to all the other work I need to do. Sorry!

🤣 that makes sense 😄

My understanding is that writes always happen by calling /web/dataset/call_kw/${model}/${method} so there's not direct call.

@guewen any opinion?

@guewen
Copy link
Copy Markdown
Member

guewen commented Jul 14, 2025

I wondered about that too when reviewing but then realized some fields can be modified through the API (from the UI), some mustn't, so I don't think we can do that without the sentinel?

@bosd
Copy link
Copy Markdown
Contributor

bosd commented Aug 15, 2025

After this fix I am getting a:
AttributeError: module 'odoo.api' has no attribute 'private'

RPC_ERROR

Odoo Server Error

Occured on localhost:18069 on model ir.module.module and id 9 on 2025-08-15 13:22:59 GMT

Traceback (most recent call last):
  File "/opt/odoo/custom/src/odoo/odoo/http.py", line 1957, in _transactioning
    return service_model.retrying(func, env=self.env)
  File "/opt/odoo/custom/src/odoo/odoo/service/model.py", line 137, in retrying
    result = func()
  File "/opt/odoo/custom/src/odoo/odoo/http.py", line 1924, in _serve_ir_http
    response = self.dispatcher.dispatch(rule.endpoint, args)
  File "/opt/odoo/custom/src/odoo/odoo/http.py", line 2171, in dispatch
    result = self.request.registry['ir.http']._dispatch(endpoint)
  File "/opt/odoo/custom/src/odoo/odoo/addons/base/models/ir_http.py", line 329, in _dispatch
    result = endpoint(**request.params)
  File "/opt/odoo/custom/src/odoo/odoo/http.py", line 727, in route_wrapper
    result = endpoint(self, *args, **params_ok)
  File "/opt/odoo/auto/addons/web/controllers/dataset.py", line 40, in call_button
    action = call_kw(request.env[model], method, args, kwargs)
  File "/opt/odoo/custom/src/odoo/odoo/api.py", line 517, in call_kw
    result = getattr(recs, name)(*args, **kwargs)
  File "<decorator-gen-73>", line 2, in button_immediate_install
  File "/opt/odoo/custom/src/odoo/odoo/addons/base/models/ir_module.py", line 75, in check_and_log
    return method(self, *args, **kwargs)
  File "/opt/odoo/custom/src/odoo/odoo/addons/base/models/ir_module.py", line 480, in button_immediate_install
    return self._button_immediate_function(self.env.registry[self._name].button_install)
  File "/opt/odoo/custom/src/odoo/odoo/addons/base/models/ir_module.py", line 604, in _button_immediate_function
    registry = modules.registry.Registry.new(self._cr.dbname, update_module=True)
  File "<decorator-gen-13>", line 2, in new
  File "/opt/odoo/custom/src/odoo/odoo/tools/func.py", line 97, in locked
    return func(inst, *args, **kwargs)
  File "/opt/odoo/custom/src/odoo/odoo/modules/registry.py", line 127, in new
    odoo.modules.load_modules(registry, force_demo, status, update_module)
  File "/opt/odoo/custom/src/odoo/odoo/modules/loading.py", line 480, in load_modules
    processed_modules += load_marked_modules(env, graph,
  File "/opt/odoo/custom/src/odoo/odoo/modules/loading.py", line 364, in load_marked_modules
    loaded, processed = load_module_graph(
  File "/opt/odoo/custom/src/odoo/odoo/modules/loading.py", line 185, in load_module_graph
    load_openerp_module(package.name)
  File "/opt/odoo/custom/src/odoo/odoo/modules/module.py", line 384, in load_openerp_module
    __import__(qualname)
  File "/opt/odoo/auto/addons/queue_job/__init__.py", line 3, in <module>
    from . import models
  File "/opt/odoo/auto/addons/queue_job/models/__init__.py", line 3, in <module>
    from . import queue_job
  File "/opt/odoo/auto/addons/queue_job/models/queue_job.py", line 31, in <module>
    class QueueJob(models.Model):
  File "/opt/odoo/auto/addons/queue_job/models/queue_job.py", line 238, in QueueJob
    @api.private
AttributeError: module 'odoo.api' has no attribute 'private'

The above server error caused the following client error:
RPC_ERROR: Odoo Server Error
    RPCError@http://localhost:18069/web/assets/0c704c8/web.assets_web.min.js:3121:338
    makeErrorFromResponse@http://localhost:18069/web/assets/0c704c8/web.assets_web.min.js:3124:163
    rpc._rpc/promise</<@http://localhost:18069/web/assets/0c704c8/web.assets_web.min.js:3129:34
    

@guewen
Copy link
Copy Markdown
Member

guewen commented Apr 24, 2026

Hi @amh-mw, I just looked at @api.private for another case where I wanted to protect create/write from RPC and I think it does not bring the protection the sentinel method was giving. @api.private will effectively remove create from the public API and prevent any direct call to create, but for e.g. it will not protect against creating jobs through /web/dataset/call_kw/queue.job/web_save, right?

@amh-mw
Copy link
Copy Markdown
Member Author

amh-mw commented Apr 24, 2026

That seems unfortunately right. My knee jerk reaction is to override web_save and restore the sentinel there, but then I wonder if other bypasses exist.

@guewen
Copy link
Copy Markdown
Member

guewen commented Apr 24, 2026

My knee jerk reaction is to override web_save and restore the sentinel there, but then I wonder if other bypasses exist.

I had the exact same reasoning

@amh-mw
Copy link
Copy Markdown
Member Author

amh-mw commented Apr 24, 2026

After digging a bit more, it looks like the /web/dataset/call_kw/queue.job/web_save route will throw an AccessError when it detects the _api_private decoration.

Per https://github.com/odoo/odoo/blob/18.0/addons/web/controllers/dataset.py#L32-L36

    @http.route(['/web/dataset/call_kw', '/web/dataset/call_kw/<path:path>'], type='json', auth="user", readonly=_call_kw_readonly)
    def call_kw(self, model, method, args, kwargs, path=None):
        Model = request.env[model]
        get_public_method(Model, method)
        return call_kw(request.env[model], method, args, kwargs)

Per https://github.com/odoo/odoo/blob/18.0/odoo/service/model.py#L29-L46

def get_public_method(model, name):
    """ Get the public unbound method from a model.
    When the method does not exist or is inaccessible, raise appropriate errors.
    Accessible methods are public (in sense that python defined it:
    not prefixed with "_") and are not decorated with `@api.private`.
    """
    assert isinstance(model, BaseModel), f"{model!r} is not a BaseModel for {name}"
    cls = type(model)
    method = getattr(cls, name, None)
    if not callable(method):
        raise AttributeError(f"The method '{model._name}.{name}' does not exist")  # noqa: TRY004
    for mro_cls in cls.mro():
        cla_method = getattr(mro_cls, name, None)
        if not cla_method:
            continue
        if name.startswith('_') or getattr(cla_method, '_api_private', False) or name in _UNSAFE_ATTRIBUTES:
            raise AccessError(f"Private methods (such as '{model._name}.{name}') cannot be called remotely.")
    return method

@amh-mw
Copy link
Copy Markdown
Member Author

amh-mw commented Apr 24, 2026

Argh, no -- I'm reading it wrong, because web_save does not have the decoration, only create does.

Comment thread queue_job/tests/test_queue_job_protected_write.py
@guewen
Copy link
Copy Markdown
Member

guewen commented Apr 29, 2026

@amh-mw wait, actually no user should have permissions to create jobs. Queue job managers have permissions to create and delete that should be removed IMO. In older versions, the admin user could bypass the permissions so the sentinel was needed, but now that access rights are enforced even for the admin user, it seems the cleanest and simplest solution (and they should never have had these permissions anyway). Does it sound right to you? If yes, I can open a PR for this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants