Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC][POC] new XMLRPC endpoint #3989

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

xmo-odoo
Copy link
Collaborator

@xmo-odoo xmo-odoo commented Dec 2, 2014

The current XMLRPC endpoints (/xmlrpc/ and /xmlrpc/2/) use the old calling conventions (CC). They can't be converted to the new CC as there is not enough information at their level to do so (knowledge of whether/where ids and contexts are). This means new-CC methods must currently be decorated with @model or @multi to be exposed over RPC, or calling them raises a TypeError (number of parameters does not match). The old CC can't be deprecated/removed until an RPC endpoint using the new CC is available.

At the same time, while working on the webservice doc I found out Odoo's xmlrpc layer is pretty idiosyncratic and doesn't really take advantage of xmlrpc and the facilities provided by xmlrpc libraries. The endpoint switch caused by the new CC is the occasion to explore/improve that part as well.

Problems of the existing endpoint / system

  1. does RPC over RPC (implements its own Odoo RPC over XML-RPC) so can't take advantage of library conveniences when they exist
  2. only works with dicts and lists, not recordset or other types of mappings and sequences
  3. doesn't allow None/nil/null, will break clients if support is enabled, this is an issue because json-rpc serialises None by default so devs lose the habit of returning a dummy placeholder as it works fine from the web client
  4. non-standard authentication

Calling Conventions (improves on 1 and 2)

a_call(subject[, args:list][, kwargs:dict])

Splits the ids (if any) and context (if any) from positional and keyword arguments in order to better match the "new API" calling conventions (where these informations are respectively part of the recordset and environment not the method call). In the RPC layer, these two items are part of the subject positional argument which can be:

  • falsy, in which case the method is called on an empty recordset with a context-less environment
  • a list, which is assumed to be a list of record ids used for the subject recordset
  • a mapping which may provide the keys records and context. The records key is browsed (or ignored if missing/empty) and the context key is used to seed the Environment (or ignored if missing)

This scheme seems to provide the best flexibility and terseness, as well as a measure of extensibility by allowing / using more keys from the subject mapping in the future.

Like execute_kw, calls then take a args list and a kwargs dict, both are optional rather than just the kwargs: a method can be called with only args, only kwargs, both or neither.

Custom XMLRPC serializer (improves on 2)

New CC methods can return recordsets, which the new RPC endpoint has to automatically convert somehow, the easiest was to serialise recordsets as lists of ids (that's coherent with the old/new API decorators, and means e.g. search returns the same thing).

Customizing xmlrpclib.Marshaller didn't work because it dispatches by strict type equality, so it wasn't possible to hook into it to serialise arbitrary recordsets (they get types created on-the-fly). A new marshaller was reimplemented on top of lxml instead. It provides the following features not provided by xmlrpclib.Marshaller:

  • serialises recordsets to lists of ids, uses BaseModel.ids (ignores NewId)
  • converts all mapping keys to strings (same as json.dumps) instead of raising an error (see XmlRcp error: dictionary key must be string #12667 and probably others I didn't find)
  • can serialise arbitrary mappings (e.g. Counter, defaultdict) not just dict
  • can serialise arbitrary iterables, not just list and tuple

Allow None/nil/null (fixes 3)

Of the 4 libraries we have examples for, 3 either enable the feature by default or via a simple flag. The last one (Apache XML-RPC) can be coerced into supporting it using ~15 lines of code which is downright terse as far as the combination of Java and dynamically typed RPC goes.

This allows more natural returns from methods (aka don't return anything instead of the artificial unconditional return True and return False peppered through the codebase) and is in line with JSON-RPC.

Endpoint (avoids conflict with existing)

Uses /RPC2:

Authentication (fixes 4, also improves on 1)

All xmlrpc libraries surveyed provide built-in support for Basic Authentication. The new endpoint thus replaces the custom authentication by BA

  • no less (or more) secure than the existing scheme: relies on underlying transport being HTTPS for security
  • avoids an extraneous auth step to resolve a uid (although that means if the "current user"'s uid is needed it has to be resolved separately)
  • no need to keep uid/password around, they're embedded in the xmlrpc proxy object
  • easy to integrate (Basic sends "cleartext" username and password, they're just base64-encoded)

Leverage XML-RPC conventions (improves on 1)

XML-RPC methodName is conventionally a dotted path (e.g. system.listMethods), which some libraries (ripcord, xmlrpclib) can map onto the language's own dereferencing/attribute access. This maps nicely to Odoo's models: the last segment of the methodName is the method itself, the rest is the model name. This means read for account.account can be called as:

a_db.account.account.read(…)

Ruby's stdlib XMLRPC::Client has no separation between dereferencing and method call, but it's still possible to proxy to a model then call a number of methods on it e.g.

partner = a_db.proxy('res.partner')
partner.read(...)
partner.write(...)

Because authentication depends on the db (or lack thereof), the database name is part of the endpoint URL (query parameter)

  • Global methods (e.g. create/drop database) are done on a DB-less endpoint
  • DB-specific methods (none currently but maybe printing reports? Sending workflow signals?) are function calls on DB'd endpoint (straight methodName without dots)
  • method calls on models are calls on DB'd endpoint with at least one dot in methodName

Code comparison

setup

common = xmlrpclib.ServerProxy('https://{}/xmlrpc/2/common'.format(domain))
uid = common.authenticate(db, username, password, {})
models = xmlrpclib.ServerProxy('https://{}/xmlrpc/2/object'.format(domain))
partners = lambda *args: models.execute_kw(db, uid, password, 'res.partner', *args)
partners('read', …)

becomes

db = xmlrpclib.ServerProxy('https://{}:{}@{}/RPC2/{}'.format(username, password, domain, db))
partners = db.res.partner
partners.read(…)

or in Ruby

common = XMLRPC::Client.new(host, "/xmlrpc/2/common", 8069)
uid = common.authenticate(db, username, password, {})
models = XMLRPC::Client.new(host, "/xmlrpc/2/object", 8069)
partners = lambda {|*args| models.execute_kw(db, uid, password, 'res.partner', *args) }
partners.call('read', )

becomes

db = XMLRPC::Client.new(host, '/RPC2/' + db, 8069, nil, nil, username, password)
partners = db.proxy('res.partner')
partners.read()

simple method call

models.execute_kw(db, uid, password, 'res.partner', 'check_access_rights', ['read'], {'raise_exception': False})

becomes

partners.check_access_rights((), ['read'], {'raise_exception': False})

search

models.execute_kw(db, uid, password, 'res.partner', 'search', [[['is_company', '=', True], ['customer', '=', True]]])

becomes

partners.search((), [[['is_company', '=', True], ['customer', '=', True]]])

read

models.execute_kw(db, uid, password, 'res.partner', 'read', [ids])

becomes

partners.read(ids)

JSON-RPC ISO

The endpoint also supports JSON-RPC2 with the same signature, using basic auth. The dispatching is performed based on the request's mime type.

Nota: maybe the XML-RPC serialiser should reject datetimes and binaries? These are not types JSON understands...

Possible additions/reflexions

  • standard extension system.multicall, allows performing multiple independent calls over a single RPC requests, has somewhat built-in support on all reviewed RPC libraries (Python's stdlib, Ruby's stdlib, PHP's Ripcord and Java's Apache XML-RPC)

  • similar but non-standard extensions system.cascade and system.sequence, the first would allow a sequence of ~independent calls to the same subject, the latter would instead be a call chain, where call 2 would be performed on the result of call 1 e.g.

    cascade(subject, action_confirm, read)
    

    would call "action_confirm()" then "read()" on the subject but

    sequence(subject, mapped('partner_ids'), read)
    

    would get the subject's associated partners then read those.

/cc @odony @rco-odoo

@xmo-odoo xmo-odoo added RD research & development, internal work need review Wishlist labels Dec 2, 2014
@KangOl
Copy link
Contributor

KangOl commented Dec 2, 2014

Why written as a module? This can be done in the core.

@xmo-odoo
Copy link
Collaborator Author

xmo-odoo commented Dec 2, 2014

Because it's a proof of concept, that way I don't have to deal with conflicts if it sits here for 3 years.

@xmo-odoo xmo-odoo changed the title [ADD] new XMLRPC endpoint POC [RFC] new XMLRPC endpoint POC Dec 3, 2014
@xmo-odoo xmo-odoo changed the title [RFC] new XMLRPC endpoint POC [RFC][POC] new XMLRPC endpoint Dec 3, 2014
@antonylesuisse antonylesuisse self-assigned this Dec 20, 2014
@antonylesuisse
Copy link
Contributor

@xmo-odoo we nned to discuss this with @odony when you come back. I think it should be address in a much more simple and backward compatible way.

@@ -457,8 +457,7 @@ def check_registry_signaling(cls, db_name):
changed = False
if openerp.multi_process and db_name in cls.registries:
registry = cls.get(db_name)
cr = registry.cursor()
try:
with registry.cursor() as cr:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Should be with(closing(registry.cursor())) for identical behaviour, otherwise the transaction will be committed. Shouldn't have any impact though, possibly slightly less traffic (cursors are rollbacked when closed, so a regular with cursor will commit then rollback I think)

@xmo-odoo xmo-odoo force-pushed the master-rpc2-xmo branch 5 times, most recently from 41b5657 to 3f4ed7c Compare November 9, 2016 14:37
@xmo-odoo xmo-odoo changed the base branch from master to 10.0 November 24, 2016 14:44
@xmo-odoo xmo-odoo force-pushed the master-rpc2-xmo branch 3 times, most recently from ada463e to 325ed41 Compare November 29, 2016 10:32
Julien00859 pushed a commit to odoo-dev/odoo that referenced this pull request Dec 21, 2022
The current XMLRPC endpoints (`/xmlrpc/` and `/xmlrpc/2/`) use the old
calling conventions (CC) of odoo-7. They can't be converted to the new
CC of the "new" odoo-8 ORM API as there  is not enough information at
their level to do so (knowledge of whether/where ids and contexts are).

This means new-CC methods must currently be decorated with `@model` to
be exposed over RPC, or calling them raises a TypeError (number of
parameters does not match). The old CC can't be deprecated/removed
until an RPC endpoint using the new CC is available.

At the same time, while working on the webservice documentation, it
turns out that Odoo's xmlrpc layer is pretty idiosyncratic and doesn't
really take advantage of xmlrpc and the facilities provided by xmlrpc
libraries. We use the opportunity of defining a new endpoint switch to
explore/improvate that part as well.

Problems of the existing endpoint / system
==========================================

1. does RPC over RPC (implements its own Odoo RPC over XML-RPC) so
   can't take advantage of library conveniences when they exist
2. only works with dicts and lists, not recordset or other types of
   mappings and sequences
3. doesn't allow None/nil/null, will break clients if support is
   enabled, this is an issue because json-rpc serialises None by default
   so devs lose the habit of returning a dummy placeholder as it works
   fine from the web client
4. user a non-standard authentication scheme

Calling Conventions (improves on 1 and 2)
=========================================

    call('admin_function', {
        'args': [...],
        'kwargs': {...},
    })

    call('model.method', {
        'records': [...],
        'context': {...},
        'args': [...],
        'kwargs': {...},
    })

Each call now uses a single parameter, a dictionary with records,
context, args (python positional arguments) and kwargs (python named
arguments), all four values are optional.

The records and context are extracted by the server to build the
environment and recordset, the args and kwargs are used when calling the
function.

This scheme makes it clear what each argument is for and brings the
missing abstraction RPC abstraction needed for the "new" odoo-8 API.

Even if it is not strictly necessary, it has been decided to use the
same scheme also for admin functions (e.g. database management).

Custom XMLRPC serializer (improves on 2)
========================================

New CC methods can return recordsets, which the new RPC endpoint has to
automatically convert somehow, the easiest was to serialise recordsets
as lists of ids (that's coherent with the old/new API decorators, and
means e.g. `search` returns the same thing).

Customizing `xmlrpclib.Marshaller` didn't work because it dispatches by
strict type equality, so it wasn't possible to hook into it to serialise
arbitrary recordsets (they get types created on-the-fly). A new
marshaller was reimplemented on top of lxml instead. It provides the
following features not provided by `xmlrpclib.Marshaller`:

* serialises recordsets to lists of ids, uses `BaseModel.ids` (ignores
  NewId)
* converts all mapping keys to strings (same as `json.dumps`) instead
  of raising an error (see odoo#12667 and probably others I didn't find)
* can serialise arbitrary mappings (e.g. `Counter`, `defaultdict`) not
  just `dict`
* can serialise arbitrary iterables, not just `list` and `tuple`

Allow None/nil/null (fixes 3)
=============================

Of the 5 libraries we have examples for, 4 either enable the feature by
default or via a simple flag. The last one (Apache XML-RPC) can be
coerced into supporting it using ~15 lines of code which is downright
terse as far as the combination of Java and dynamically typed RPC goes.

This allows more natural returns from methods (aka don't return anything
instead of the artificial unconditional `return True` and `return False`
peppered through the codebase) and is in line with JSON-RPC.

Endpoint (avoids conflict with existing)
========================================

Uses `/RPC2`:

* does not conflict with existing endpoints
* used for the examples in the [XML-RPC specification][1]
* a number of documents call it the conventional XML-RPC path
* at least [one stdlib defaults to /RPC2][Ruby XMLRPC] when no `path`
  is provided

Authentication (fixes 4, also improves on 1)
============================================

All xmlrpc libraries surveyed provide built-in support for [Basic
Authentication](http://en.wikipedia.org/wiki/Basic_access_authentication).
The new endpoint thus replaces the custom authentication by BA

* no less (or more) secure than the existing scheme: relies on
  underlying transport being HTTPS for security
* avoids an extraneous auth step to resolve a uid (although that means
  if the "current user"'s uid is needed it has to be resolved separately)
* no need to keep uid/password around, they're embedded in the xmlrpc
  proxy object
* easy to integrate (Basic sends "cleartext" username and password,
  they're just base64-encoded)

Leverage XML-RPC conventions (improves on 1)
============================================

XML-RPC `methodName` is conventionally a dotted path (e.g.
`system.listMethods`), which most libraries can map onto the language's
own dereferencing/attribute access. This maps nicely to Odoo's models:
the last segment of the `methodName` is the method itself, the rest is
the model name. This means `read` for `account.account` can be called as:

    a_db.account.account.read(...)

Ruby's stdlib XMLRPC::Client has no separation between dereferencing and
method call, but it's still possible to proxy to a model then call a
number of methods on it e.g.

    partner = a_db.proxy('res.partner')
    partner.read(...)
    partner.write(...)

Because authentication depends on the db (or lack thereof), the database
name is part of the endpoint URL (query parameter)

* Admin methods (e.g. create/drop database) are done on a DB-less
endpoint

* Method calls on models are calls on DB'd endpoint with at least one
dot in methodName

Code comparison
===============

setup

    common = xmlrpc.client.ServerProxy('https://{}/xmlrpc/2/common'.format(domain))
    uid = common.authenticate(db, username, password, {})
    models = xmlrpc.client.ServerProxy('https://{}/xmlrpc/2/object'.format(domain))
    partners = lambda *args: models.execute_kw(db, uid, password, 'res.partner', *args)
    partners('read', ...)

becomes

    db = xmlrpclib.ServerProxy(f'https://{username}:{password}@{domain}/RPC2/{db}')
    partners = db.res.partner
    partners.read(...)

or in Ruby

    common = XMLRPC::Client.new(host, "/xmlrpc/2/common", 8069)
    uid = common.authenticate(db, username, password, {})
    models = XMLRPC::Client.new(host, "/xmlrpc/2/object", 8069)
    partners = lambda {|*args| models.execute_kw(db, uid, password, 'res.partner', *args) }
    partners.call('read', …)

becomes

    db = XMLRPC::Client.new(host, '/RPC2/' + db, 8069, nil, nil, username, password)
    partners = db.proxy('res.partner')
    partners.read(…)

simple method call

    models.execute_kw(
        db, uid, password,
        'res.partner', 'check_access_rights',
        ['read'], {'raise_exception': False})

becomes

    partners.check_access_rights({
        'args': ['read'],
        'kwargs': {'raise_exception': False}
    })

search

    models.execute_kw(
        db, uid, password,
        'res.partner', 'search',
        [[['is_company', '=', True], ['customer', '=', True]]])

becomes

    partners.search({'kwargs': {
        'domain': [['is_company', '=', True], ['customer', '=', True]]
    }})

read

    models.execute_kw(
        db, uid, password,
        'res.partner', 'read',
        [ids])

becomes

    partners.read({'records': ids})

Co-authored-by: Julien Castiaux <juc@odoo.com>

Closes odoo#3989

[XML-RPC specification]: http://xmlrpc.com/
[Ruby XMLRPC]: http://www.ruby-doc.org/stdlib-2.1.5/libdoc/xmlrpc/rdoc/XMLRPC/Client.html#method-c-new
@tde-banana-odoo
Copy link
Contributor

@xmo-odoo Any news on this ? I heard it should be compatible with Python 1.3.

cc @Julien00859 who is that kind of guy who still works with python 1.

@tde-banana-odoo
Copy link
Contributor

@xmo-odoo we nned to discuss this with @odony when you come back. I think it should be address in a much more simple and backward compatible way.

Did you discuss ?

@tivisse
Copy link
Contributor

tivisse commented Jun 14, 2023

Should be merged in 17.0 once the small conflict with http.py is resolved.

#neverstopeveloving

@tivisse
Copy link
Contributor

tivisse commented Jun 14, 2023

Because it's a proof of concept, that way I don't have to deal with conflicts if it sits here for 3 years.

Well, it's been 9 years and you do have conflicts ¯_(ツ)_/¯

@tde-banana-odoo
Copy link
Contributor

It seems your last rebase did not go well, could you do it again ? There are still some conflicts.

@Yenthe666
Copy link
Collaborator

By now this pull request deserves some special medal or award I believe 😄 Waiting for the 10-year party in december!

xmo-odoo added a commit to odoo-dev/odoo that referenced this pull request Jul 31, 2024
This monkeypatching causes issues with the XMLRPC testing, since those
run in process after loading the module they use the monkeypatched
marshaller and thus perform non-standard calls / marshalling.

This patching does not seem at all necessary as as far as I can tell
we're only dumping to xmlrpc in this specific location, and so can
just use the custom marshaller directly.

It's also very weird to monkeypatch from a controller submodule in the
depth of base.

Add a simplified `dumps` wrapper for convenience, it wraps the dumped
content into the default response since that's the only thing we need
it for. Avoids copy/pasting the marshalling code in multiple
locations.

Note that we do need a marshaller instance per dump call because it
has some local state (`memo`) in order to detect recursive content, we
could probably rewrite the thing with an explicit memo parameter
through the stack but it's likely not necessary (and if we did that it
should be in odoo#3989 anyway, using a direct-threaded `memo` parameter
was mentioned).
robodoo pushed a commit that referenced this pull request Aug 1, 2024
This monkeypatching causes issues with the XMLRPC testing, since those
run in process after loading the module they use the monkeypatched
marshaller and thus perform non-standard calls / marshalling.

This patching does not seem at all necessary as as far as I can tell
we're only dumping to xmlrpc in this specific location, and so can
just use the custom marshaller directly.

It's also very weird to monkeypatch from a controller submodule in the
depth of base.

Add a simplified `dumps` wrapper for convenience, it wraps the dumped
content into the default response since that's the only thing we need
it for. Avoids copy/pasting the marshalling code in multiple
locations.

Note that we do need a marshaller instance per dump call because it
has some local state (`memo`) in order to detect recursive content, we
could probably rewrite the thing with an explicit memo parameter
through the stack but it's likely not necessary (and if we did that it
should be in #3989 anyway, using a direct-threaded `memo` parameter
was mentioned).

Part-of: #175104
Signed-off-by: Xavier Morel (xmo) <xmo@odoo.com>
@tde-banana-odoo
Copy link
Contributor

v18 is near ! Is it merged ?

@william-andre
Copy link
Contributor

Only 7 files conflicting, mostly doc.
Should be fixed in a few seconds

@tde-banana-odoo
Copy link
Contributor

@Julien00859 Could you rebase and r+ ?

dhrs-odoo pushed a commit to odoo-dev/odoo that referenced this pull request Aug 5, 2024
This monkeypatching causes issues with the XMLRPC testing, since those
run in process after loading the module they use the monkeypatched
marshaller and thus perform non-standard calls / marshalling.

This patching does not seem at all necessary as as far as I can tell
we're only dumping to xmlrpc in this specific location, and so can
just use the custom marshaller directly.

It's also very weird to monkeypatch from a controller submodule in the
depth of base.

Add a simplified `dumps` wrapper for convenience, it wraps the dumped
content into the default response since that's the only thing we need
it for. Avoids copy/pasting the marshalling code in multiple
locations.

Note that we do need a marshaller instance per dump call because it
has some local state (`memo`) in order to detect recursive content, we
could probably rewrite the thing with an explicit memo parameter
through the stack but it's likely not necessary (and if we did that it
should be in odoo#3989 anyway, using a direct-threaded `memo` parameter
was mentioned).

Part-of: odoo#175104
Signed-off-by: Xavier Morel (xmo) <xmo@odoo.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
RD research & development, internal work RFC Request For Comment, all feedback is welcome Wishlist
Projects
None yet
Development

Successfully merging this pull request may close these issues.