Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 31 additions & 31 deletions dms/models/directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import base64
import logging
import os
from ast import literal_eval
from collections import defaultdict
from typing import Literal # noqa # pylint: disable=unused-import

Expand Down Expand Up @@ -88,7 +87,7 @@ def _default_parent_id(self):
if context.get("active_model") == self._name and context.get("active_id"):
return context["active_id"]
else:
return False
return context.get("dms_parent_id")

group_ids = fields.Many2many(
comodel_name="dms.access.group",
Expand Down Expand Up @@ -380,8 +379,8 @@ def _search_panel_directory(self, **kwargs):
@api.model
def _search_starred(self, operator, operand):
if operator in ("=", "in") and operand:
return [("user_star_ids", "in", [self.env.uid])]
return [("user_star_ids", "not in", [self.env.uid])]
return Domain([("user_star_ids", "in", [self.env.uid])])
return Domain([("user_star_ids", "not in", [self.env.uid])])

@api.depends("name", "parent_id.complete_name")
def _compute_complete_name(self):
Expand Down Expand Up @@ -490,8 +489,12 @@ def _compute_parent_id(self):
if record.is_root_directory:
record.parent_id = None
else:
# HACK: Not needed in v14 due to odoo/odoo#64359
record.parent_id = record.parent_id
record.parent_id = (
record.parent_id
or record._origin.parent_id
or self.env.context.get("default_parent_id")
or self.env.context.get("dms_parent_id")
)

@api.depends("is_root_directory", "parent_id")
def _compute_root_id(self):
Expand Down Expand Up @@ -753,37 +756,34 @@ def _search_panel_domain_image(
def action_dms_directories_all_directory(self):
self.ensure_one()
action = self.env["ir.actions.act_window"]._for_xml_id(
"dms.action_dms_directory"
)
domain = Domain.AND(
[
literal_eval(action["domain"].strip()),
[("parent_id", "child_of", self.id)],
]
"dms.action_dms_directories_all_directory"
)
action["display_name"] = self.name
action["domain"] = domain
action["context"] = dict(
self.env.context,
default_parent_id=self.id,
searchpanel_default_parent_id=self.id,
)
action["domain"] = [
("parent_id", "child_of", self.id),
("is_hidden", "=", False),
("id", "!=", self.id),
]
action["context"] = {
"default_parent_id": self.id,
"dms_parent_id": self.id,
"searchpanel_default_parent_id": self.id,
}
return action

def action_dms_files_all_directory(self):
self.ensure_one()
action = self.env["ir.actions.act_window"]._for_xml_id("dms.action_dms_file")
domain = Domain.AND(
[
literal_eval(action["domain"].strip()),
[("directory_id", "child_of", self.id)],
]
action = self.env["ir.actions.act_window"]._for_xml_id(
"dms.action_dms_files_all_directory"
)
action["display_name"] = self.name
action["domain"] = domain
action["context"] = dict(
self.env.context,
default_directory_id=self.id,
searchpanel_default_directory_id=self.id,
)
action["domain"] = [
("directory_id", "child_of", self.id),
("is_hidden", "=", False),
]
action["context"] = {
"default_directory_id": self.id,
"dms_directory_id": self.id,
"searchpanel_default_directory_id": self.id,
}
return action
3 changes: 3 additions & 0 deletions dms/models/dms_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class DMSFile(models.Model):
required=True,
index="btree",
tracking=True, # Leave log if "moved" to another directory
default=lambda self: self.env.context.get("dms_directory_id"),
)
root_directory_id = fields.Many2one(related="directory_id.root_directory_id")
# Override acording to defined in AbstractDmsMixin
Expand Down Expand Up @@ -568,6 +569,8 @@ def _create_model_attachment(self, vals):
directory_id = self.env.context.get("active_id")
elif self.env.context.get("default_directory_id"):
directory_id = self.env.context.get("default_directory_id")
elif self.env.context.get("dms_directory_id"):
directory_id = self.env.context.get("dms_directory_id")
directory = self.env["dms.directory"].browse(directory_id)
if (
directory.res_model
Expand Down
3 changes: 2 additions & 1 deletion dms/models/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from odoo import api, fields, models
from odoo.exceptions import AccessError
from odoo.fields import Domain

_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -88,7 +89,7 @@ class Storage(models.Model):

def _search_model(self, operator, value):
allowed_items = self.env["ir.model"].sudo().search([("model", operator, value)])
return [("model_ids", "in", allowed_items.ids)]
return Domain([("model_ids", "in", allowed_items.ids)])

@api.onchange("save_type")
def _onchange_save_type(self):
Expand Down
15 changes: 14 additions & 1 deletion dms/tests/test_directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from odoo import Command
from odoo.exceptions import AccessError, UserError
from odoo.tests import new_test_user
from odoo.tests import Form, new_test_user
from odoo.tests.common import users
from odoo.tools import mute_logger

Expand Down Expand Up @@ -41,6 +41,19 @@ def test_create_directory(self):
msg="The root directory should have one subdirectory",
)

def test_default_parent_from_context(self):
action = self.directory.action_dms_directories_all_directory()
action_context = action["context"].copy()
action_context.pop("default_parent_id")
directory_form = Form(self.directory_model.with_context(**action_context))
self.assertEqual(directory_form.parent_id, self.directory)

action = self.directory.action_dms_files_all_directory()
action_context = action["context"].copy()
action_context.pop("default_directory_id")
file_form = Form(self.file_model.with_context(**action_context))
self.assertEqual(file_form.directory_id, self.directory)

@users("dms-manager", "dms-user")
def test_copy_root_directory(self):
copy_root_directory = self.directory.copy()
Expand Down
45 changes: 45 additions & 0 deletions dms/tests/test_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ def setUpClass(cls):
)
cls.directory_group_a.group_ids = [(4, cls.group_a.id)]
cls.file2 = cls.create_file(directory=cls.sub_directory_x)
cls.readonly_user = new_test_user(
cls.env, login="read-only", groups="dms.group_dms_user"
)
cls.readonly_group = cls.access_group_model.create(
{
"name": "Read only",
"explicit_user_ids": [(6, 0, [cls.readonly_user.id])],
}
)
cls.readonly_directory = cls.create_directory(storage=cls.storage)
cls.readonly_directory.group_ids = [(6, 0, cls.readonly_group.ids)]
cls.readonly_subdirectory = cls.create_directory(
directory=cls.readonly_directory
)
cls.readonly_file = cls.create_file(directory=cls.readonly_subdirectory)

@users("user-a")
def test_unaccessible_file(self):
Expand Down Expand Up @@ -140,6 +155,36 @@ def test_record_level_access(self):
}
)

@users("read-only")
@mute_logger("odoo.addons.base.models.ir_rule", "odoo.models")
def test_read_only_access(self):
"""Read-only access groups must not grant mutation permissions."""
readonly_file = self.readonly_file.with_user(self.env.user)
readonly_file.check_access("read")
for operation in ("write", "unlink"):
with self.assertRaises(
AccessError, msg=f"read-only user {operation} must be denied"
):
readonly_file.check_access(operation)
with self.assertRaises(AccessError, msg="read-only file write must fail"):
readonly_file.write({"name": "forbidden.txt"})
with self.assertRaises(AccessError, msg="read-only file create must fail"):
self.file_model.with_user(self.env.user).create(
{
"name": "forbidden.txt",
"directory_id": self.readonly_subdirectory.id,
"content": self.content_base64(),
}
)
with self.assertRaises(AccessError, msg="read-only directory create must fail"):
self.directory_model.with_user(self.env.user).create(
{
"name": "Forbidden child",
"is_root_directory": False,
"parent_id": self.readonly_directory.id,
}
)

@users("dms-manager", "dms-user")
@mute_logger("odoo.models.unlink")
def test_content_file(self):
Expand Down
43 changes: 43 additions & 0 deletions dms/tests/test_portal.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# Copyright 2021-2025 Tecnativa - Víctor Martínez
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl)

import json

import odoo.tests
from odoo import http
from odoo.exceptions import AccessError
from odoo.tests.common import new_test_user, users
from odoo.tools import mute_logger
Expand Down Expand Up @@ -86,6 +89,46 @@ def test_access_portal(self):
self.assertEqual(
response.status_code, 200, "Can access directory with correct access_token"
)
# A direct binary URL must not bypass DMS access checks.
response = self.url_open(
f"/web/content?id={self.other_file_partner.id}&field=content"
"&model=dms.file&filename_field=name&download=true",
timeout=20,
)
self.assertNotEqual(
response.status_code,
200,
"Can't download a restricted file through a direct URL",
)

def test_upload_and_download(self):
upload_directory = self.create_directory(storage=self.create_storage())
self.authenticate("dms-manager", "dms-manager")
response = self.url_open(
"/web/binary/upload_dms_file",
data={
"csrf_token": http.Request.csrf_token(self),
"directory_id": upload_directory.id,
},
files={
"ufile": (
"uploaded.txt",
b"Odoo 19 DMS upload",
"text/plain",
)
},
)
response.raise_for_status()
result = json.loads(response.content)
self.assertFalse(result[0].get("error"))
dms_file = self.file_model.browse(result[0]["id"])
self.assertEqual(dms_file.name, "uploaded.txt")
download = self.url_open(
f"/web/content?id={dms_file.id}&field=content&model=dms.file"
"&filename_field=name&download=true"
)
download.raise_for_status()
self.assertEqual(download.content, b"Odoo 19 DMS upload")

def test_tour(self):
for tour in ("dms_portal_mail_tour", "dms_portal_partners_tour"):
Expand Down
5 changes: 4 additions & 1 deletion dms/views/dms_directory.xml
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,10 @@
widget="many2many_tags"
options="{'color_field': 'color'}"
/>
<field name="write_date" widget="date" />
<field
name="write_date"
options="{'show_time': false}"
/>
</div>
</div>
<footer class="mt-auto pt-0 ms-1">
Expand Down
5 changes: 4 additions & 1 deletion dms/views/dms_file.xml
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,10 @@
widget="many2many_tags"
options="{'color_field': 'color'}"
/>
<field name="write_date" widget="date" />
<field
name="write_date"
options="{'show_time': false}"
/>
</div>
<footer class="mt-auto pt-0 ms-1">
<div class="d-flex ms-auto oe_kanban_bottom_right">
Expand Down
Loading