[ADD] ir.attachment: add token field for external access

Introduce a new attachment field (access_token) to allow external
unauthenticated access. This will be an opaque unique number
(typically a UUID) that should be provided via an appropriate
controller, for unauthenticated display.

The field is intended to be NULL unless unauthenticated access has been
allowed, in which case a value will be set for the access_token.

This could be used e.g. for allowing access to images within mailings,
even when the recipient is not logged in (which is sometimes entirely
impossible, when email providers use restricted proxy servers to
load images)

Note 1: this is still a work-in-progress, but serves to freeze the API.
The implementation of the access check and provisioning of the new
field will be added later.

Note 2: namimg collisions with the file download token prevent the use
of a shorter 'token' parameter for download routes.

Apologies for the late (and incomplete) addition in saas-18 :-/
This commit is contained in:
Olivier Dony
2017-09-21 23:48:36 +02:00
parent bb0404ea30
commit 2257583bb5
4 changed files with 36 additions and 10 deletions
+20 -7
View File
@@ -417,10 +417,13 @@ def xml2json_from_elementtree(el, preserve_whitespaces=False):
res["children"] = kids
return res
def binary_content(xmlid=None, model='ir.attachment', id=None, field='datas', unique=False, filename=None, filename_field='datas_fname', download=False, mimetype=None, default_mimetype='application/octet-stream', env=None):
def binary_content(xmlid=None, model='ir.attachment', id=None, field='datas', unique=False,
filename=None, filename_field='datas_fname', download=False, mimetype=None,
default_mimetype='application/octet-stream', access_token=None, env=None):
return request.registry['ir.http'].binary_content(
xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename, filename_field=filename_field,
download=download, mimetype=mimetype, default_mimetype=default_mimetype, env=env)
xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename,
filename_field=filename_field, download=download, mimetype=mimetype,
default_mimetype=default_mimetype, access_token=access_token, env=env)
#----------------------------------------------------------
# Odoo Web web Controllers
@@ -971,8 +974,13 @@ class Binary(http.Controller):
'/web/content/<int:id>-<string:unique>/<string:filename>',
'/web/content/<string:model>/<int:id>/<string:field>',
'/web/content/<string:model>/<int:id>/<string:field>/<string:filename>'], type='http', auth="public")
def content_common(self, xmlid=None, model='ir.attachment', id=None, field='datas', filename=None, filename_field='datas_fname', unique=None, mimetype=None, download=None, data=None, token=None):
status, headers, content = binary_content(xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename, filename_field=filename_field, download=download, mimetype=mimetype)
def content_common(self, xmlid=None, model='ir.attachment', id=None, field='datas',
filename=None, filename_field='datas_fname', unique=None, mimetype=None,
download=None, data=None, token=None, access_token=None):
status, headers, content = binary_content(
xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename,
filename_field=filename_field, download=download, mimetype=mimetype,
access_token=access_token)
if status == 304:
response = werkzeug.wrappers.Response(status=status, headers=headers)
elif status == 301:
@@ -1004,8 +1012,13 @@ class Binary(http.Controller):
'/web/image/<int:id>-<string:unique>/<string:filename>',
'/web/image/<int:id>-<string:unique>/<int:width>x<int:height>',
'/web/image/<int:id>-<string:unique>/<int:width>x<int:height>/<string:filename>'], type='http', auth="public")
def content_image(self, xmlid=None, model='ir.attachment', id=None, field='datas', filename_field='datas_fname', unique=None, filename=None, mimetype=None, download=None, width=0, height=0, crop=False):
status, headers, content = binary_content(xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename, filename_field=filename_field, download=download, mimetype=mimetype, default_mimetype='image/png')
def content_image(self, xmlid=None, model='ir.attachment', id=None, field='datas',
filename_field='datas_fname', unique=None, filename=None, mimetype=None,
download=None, width=0, height=0, crop=False, access_token=None):
status, headers, content = binary_content(
xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename,
filename_field=filename_field, download=download, mimetype=mimetype,
default_mimetype='image/png', access_token=access_token)
if status == 304:
return werkzeug.wrappers.Response(status=304, headers=headers)
elif status == 301:
+8 -2
View File
@@ -202,7 +202,10 @@ class Http(models.AbstractModel):
return werkzeug.wrappers.Response(html, status=code, content_type='text/html;charset=utf-8')
@classmethod
def binary_content(cls, xmlid=None, model='ir.attachment', id=None, field='datas', unique=False, filename=None, filename_field='datas_fname', download=False, mimetype=None, default_mimetype='application/octet-stream', env=None):
def binary_content(cls, xmlid=None, model='ir.attachment', id=None, field='datas',
unique=False, filename=None, filename_field='datas_fname', download=False,
mimetype=None, default_mimetype='application/octet-stream',
access_token=None, env=None):
env = env or request.env
obj = None
if xmlid:
@@ -212,7 +215,10 @@ class Http(models.AbstractModel):
if obj and 'website_published' in obj._fields:
if env[obj._name].sudo().search([('id', '=', obj.id), ('website_published', '=', True)]):
env = env(user=SUPERUSER_ID)
return super(Http, cls).binary_content(xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename, filename_field=filename_field, download=download, mimetype=mimetype, default_mimetype=default_mimetype, env=env)
return super(Http, cls).binary_content(
xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename,
filename_field=filename_field, download=download, mimetype=mimetype,
default_mimetype=default_mimetype, access_token=access_token, env=env)
class ModelConverter(ModelConverter):
+3
View File
@@ -279,6 +279,9 @@ class IrAttachment(models.Model):
url = fields.Char('Url', index=True, size=1024)
public = fields.Boolean('Is public document')
# for external access
access_token = fields.Char('Access Token')
# the field 'datas' is computed and may use the other fields below
datas = fields.Binary(string='File Content', compute='_compute_datas', inverse='_inverse_datas')
db_datas = fields.Binary('Database Data')
+5 -1
View File
@@ -248,7 +248,10 @@ class IrHttp(models.AbstractModel):
return content_disposition(filename)
@classmethod
def binary_content(cls, xmlid=None, model='ir.attachment', id=None, field='datas', unique=False, filename=None, filename_field='datas_fname', download=False, mimetype=None, default_mimetype='application/octet-stream', env=None):
def binary_content(cls, xmlid=None, model='ir.attachment', id=None, field='datas',
unique=False, filename=None, filename_field='datas_fname', download=False,
mimetype=None, default_mimetype='application/octet-stream',
access_token=None, env=None):
""" Get file, attachment or downloadable content
If the ``xmlid`` and ``id`` parameter is omitted, fetches the default value for the
@@ -265,6 +268,7 @@ class IrHttp(models.AbstractModel):
:param bool download: apply headers to download the file
:param str mimetype: mintype of the field (for headers)
:param str default_mimetype: default mintype if no mintype found
:param str access_token: optional token for unauthenticated access
:param Environment env: by default use request.env
:returns: (status, headers, content)
"""