-
-
Notifications
You must be signed in to change notification settings - Fork 221
[18.0][IMP] storage_file: improve public/private behavior #623
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
base: 18.0
Are you sure you want to change the base?
Changes from all commits
a7aae0d
9aed6fb
8061fb0
32db6bf
da22324
c741cad
0e6efea
8ce3784
40b685a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| <?xml version="1.0" encoding="utf-8" ?> | ||
| <odoo noupdate="1"> | ||
| <record id="storage_file_backend" model="ir.config_parameter"> | ||
| <field name="key">storage.file.backend_id</field> | ||
| <field name="value" ref="storage_backend.default_storage_backend" /> | ||
| </record> | ||
| </odoo> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -145,7 +145,9 @@ def _get_base_url_from_param(self): | |
| def _get_url_for_file(self, storage_file, exclude_base_url=False): | ||
| """Return final full URL for given file.""" | ||
| backend = self.sudo() | ||
| if backend.served_by == "odoo": | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. backend.servedy_by is external but the file is served by odoo It's confusing May be add a new |
||
| if backend.served_by == "odoo" or ( | ||
| backend.served_by == "external" and not backend.base_url | ||
| ): | ||
| parts = [ | ||
| self._get_base_url_from_param() if not exclude_base_url else "/", | ||
| "storage.file", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink | ||
| access_storage_file_edit,storage_file edit,model_storage_file,base.group_system,1,1,1,1 | ||
| access_storage_file_read_public,storage_file public read,model_storage_file,base.group_user,1,0,0,0 | ||
| access_storage_file_read_portal,storage_file portal read,model_storage_file,base.group_public,1,0,0,0 | ||
| access_storage_file_replace,storage_file_replace public,model_storage_file_replace,base.group_user,1,1,1,1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,3 @@ | ||
| from . import test_storage_file | ||
| from . import test_is_public | ||
| from . import test_controller |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| # Copyright 2026 Camptocamp SA | ||
| # @author Simone Orsi <simone.orsi@camptocamp.com> | ||
| # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). | ||
|
|
||
| import base64 | ||
|
|
||
| from odoo.tests.common import HttpCase, tagged | ||
|
|
||
|
|
||
| @tagged("-at_install", "post_install") | ||
| class TestStorageFileController(HttpCase): | ||
| """Test the /storage.file/ controller with public/private and odoo/external.""" | ||
|
|
||
| @classmethod | ||
| def setUpClass(cls): | ||
| super().setUpClass() | ||
| cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) | ||
| cls.data = b"Hello, storage!" | ||
| cls.filedata = base64.b64encode(cls.data) | ||
| cls.backend_odoo_public = cls.env["storage.backend"].create( | ||
| { | ||
| "name": "Odoo Public", | ||
| "backend_type": "filesystem", | ||
| "served_by": "odoo", | ||
| "is_public": True, | ||
| "filename_strategy": "name_with_id", | ||
| } | ||
| ) | ||
| cls.backend_odoo_private = cls.env["storage.backend"].create( | ||
| { | ||
| "name": "Odoo Private", | ||
| "backend_type": "filesystem", | ||
| "served_by": "odoo", | ||
| "is_public": False, | ||
| "filename_strategy": "name_with_id", | ||
| } | ||
| ) | ||
| cls.backend_ext_public = cls.env["storage.backend"].create( | ||
| { | ||
| "name": "Ext Public (no CDN)", | ||
| "backend_type": "filesystem", | ||
| "served_by": "external", | ||
| "base_url": "", | ||
| "is_public": True, | ||
| "filename_strategy": "name_with_id", | ||
| } | ||
| ) | ||
| cls.backend_ext_private = cls.env["storage.backend"].create( | ||
| { | ||
| "name": "Ext Private (no CDN)", | ||
| "backend_type": "filesystem", | ||
| "served_by": "external", | ||
| "base_url": "", | ||
| "is_public": False, | ||
| "filename_strategy": "name_with_id", | ||
| } | ||
| ) | ||
| cls.file_odoo_public = cls.env["storage.file"].create( | ||
| { | ||
| "name": "pub-odoo.txt", | ||
| "backend_id": cls.backend_odoo_public.id, | ||
| "data": cls.filedata, | ||
| } | ||
| ) | ||
| cls.file_odoo_private = cls.env["storage.file"].create( | ||
| { | ||
| "name": "priv-odoo.txt", | ||
| "backend_id": cls.backend_odoo_private.id, | ||
| "data": cls.filedata, | ||
| } | ||
| ) | ||
| cls.file_ext_public = cls.env["storage.file"].create( | ||
| { | ||
| "name": "pub-ext.txt", | ||
| "backend_id": cls.backend_ext_public.id, | ||
| "data": cls.filedata, | ||
| } | ||
| ) | ||
| cls.file_ext_private = cls.env["storage.file"].create( | ||
| { | ||
| "name": "priv-ext.txt", | ||
| "backend_id": cls.backend_ext_private.id, | ||
| "data": cls.filedata, | ||
| } | ||
| ) | ||
| cls.internal_user = ( | ||
| cls.env["res.users"] | ||
| .with_context(no_reset_password=True) | ||
| .create( | ||
| { | ||
| "name": "Storage Test User", | ||
| "login": "storage_test_user", | ||
| "password": "storage_test_user", | ||
| "groups_id": [ | ||
| (4, cls.env.ref("base.group_user").id), | ||
| ], | ||
| } | ||
| ) | ||
| ) | ||
|
|
||
| def _url_for(self, storage_file): | ||
| return f"/storage.file/{storage_file.slug}" | ||
|
|
||
| # ---- Public user (anonymous) ---- | ||
|
|
||
| def test_public_user_odoo_public(self): | ||
| """Public user + public odoo backend -> 200.""" | ||
| resp = self.url_open(self._url_for(self.file_odoo_public)) | ||
| self.assertEqual(resp.status_code, 200) | ||
| self.assertEqual(resp.content, self.data) | ||
|
|
||
| def test_public_user_odoo_private(self): | ||
| """Public user + private odoo backend -> 404.""" | ||
| resp = self.url_open(self._url_for(self.file_odoo_private)) | ||
| self.assertEqual(resp.status_code, 404) | ||
|
|
||
| def test_public_user_ext_public(self): | ||
| """Public user + public external backend (no CDN) -> 200.""" | ||
| resp = self.url_open(self._url_for(self.file_ext_public)) | ||
| self.assertEqual(resp.status_code, 200) | ||
| self.assertEqual(resp.content, self.data) | ||
|
|
||
| def test_public_user_ext_private(self): | ||
| """Public user + private external backend (no CDN) -> 404.""" | ||
| resp = self.url_open(self._url_for(self.file_ext_private)) | ||
| self.assertEqual(resp.status_code, 404) | ||
|
|
||
| # ---- Internal (authenticated) user ---- | ||
|
|
||
| def test_internal_user_odoo_public(self): | ||
| """Internal user + public odoo backend -> 200.""" | ||
| self.authenticate("storage_test_user", "storage_test_user") | ||
| resp = self.url_open(self._url_for(self.file_odoo_public)) | ||
| self.assertEqual(resp.status_code, 200) | ||
| self.assertEqual(resp.content, self.data) | ||
|
|
||
| def test_internal_user_odoo_private(self): | ||
| """Internal user + private odoo backend -> 200.""" | ||
| self.authenticate("storage_test_user", "storage_test_user") | ||
| resp = self.url_open(self._url_for(self.file_odoo_private)) | ||
| self.assertEqual(resp.status_code, 200) | ||
| self.assertEqual(resp.content, self.data) | ||
|
|
||
| def test_internal_user_ext_public(self): | ||
| """Internal user + public external backend (no CDN) -> 200.""" | ||
| self.authenticate("storage_test_user", "storage_test_user") | ||
| resp = self.url_open(self._url_for(self.file_ext_public)) | ||
| self.assertEqual(resp.status_code, 200) | ||
| self.assertEqual(resp.content, self.data) | ||
|
|
||
| def test_internal_user_ext_private(self): | ||
| """Internal user + private external backend (no CDN) -> 200.""" | ||
| self.authenticate("storage_test_user", "storage_test_user") | ||
| resp = self.url_open(self._url_for(self.file_ext_private)) | ||
| self.assertEqual(resp.status_code, 200) | ||
| self.assertEqual(resp.content, self.data) |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,60 @@ | ||||||
| # Copyright 2026 Camptocamp SA | ||||||
| # @author Simone Orsi <simone.orsi@camptocamp.com> | ||||||
| # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). | ||||||
|
|
||||||
| from odoo.addons.component.tests.common import TransactionComponentCase | ||||||
|
|
||||||
|
|
||||||
| class TestStorageFileIsPublic(TransactionComponentCase): | ||||||
| @classmethod | ||||||
| def setUpClass(cls): | ||||||
| super().setUpClass() | ||||||
| cls.backend = cls.env["storage.backend"].create( | ||||||
| {"name": "Test backend", "backend_type": "filesystem"} | ||||||
| ) | ||||||
| cls.storage_file = cls.env["storage.file"].create( | ||||||
| { | ||||||
| "name": "test-public.txt", | ||||||
| "backend_id": cls.backend.id, | ||||||
| "data": b"aGVsbG8=", # "hello" base64 | ||||||
| } | ||||||
| ) | ||||||
|
|
||||||
| def test_reflects_backend_flag(self): | ||||||
| self.assertFalse(self.storage_file.is_public) | ||||||
| self.backend.is_public = True | ||||||
| self.storage_file.invalidate_recordset(["is_public"]) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This cache invalidation should not be necessary. Does this test still succeed if this line is removed? |
||||||
| self.assertTrue(self.storage_file.is_public) | ||||||
|
|
||||||
| def test_search_true(self): | ||||||
| self.backend.is_public = True | ||||||
| result = self.env["storage.file"].search( | ||||||
| [("is_public", "=", True), ("id", "=", self.storage_file.id)] | ||||||
| ) | ||||||
| self.assertIn(self.storage_file, result) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpicking:
Suggested change
|
||||||
|
|
||||||
| def test_search_false(self): | ||||||
| self.backend.is_public = False | ||||||
| result = self.env["storage.file"].search( | ||||||
| [("is_public", "=", False), ("id", "=", self.storage_file.id)] | ||||||
| ) | ||||||
| self.assertIn(self.storage_file, result) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpicking:
Suggested change
|
||||||
|
|
||||||
| def test_search_with_two_backends(self): | ||||||
| public_backend = self.env["storage.backend"].create( | ||||||
| { | ||||||
| "name": "Public backend", | ||||||
| "backend_type": "filesystem", | ||||||
| "is_public": True, | ||||||
| } | ||||||
| ) | ||||||
| public_file = self.env["storage.file"].create( | ||||||
| { | ||||||
| "name": "public.txt", | ||||||
| "backend_id": public_backend.id, | ||||||
| "data": b"aGVsbG8=", | ||||||
| } | ||||||
| ) | ||||||
| result = self.env["storage.file"].search([("is_public", "=", True)]) | ||||||
| self.assertIn(public_file, result) | ||||||
| self.assertNotIn(self.storage_file, result) | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why hidding AccessError ?