264 lines
9.6 KiB
Python
Executable File
264 lines
9.6 KiB
Python
Executable File
# Copyright 2020 Creu Blanca
|
|
# Copyright 2021 Tecnativa - Víctor Martínez
|
|
# Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com).
|
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
|
|
|
|
|
from logging import getLogger
|
|
|
|
from odoo import api, fields, models
|
|
from odoo.osv.expression import (
|
|
FALSE_DOMAIN,
|
|
NEGATIVE_TERM_OPERATORS,
|
|
OR,
|
|
TRUE_DOMAIN,
|
|
)
|
|
|
|
_logger = getLogger(__name__)
|
|
|
|
|
|
class DmsSecurityMixin(models.AbstractModel):
|
|
_name = "dms.security.mixin"
|
|
_description = "DMS Security Mixin"
|
|
|
|
# Submodels must define this field that points to the owner dms.directory
|
|
_directory_field = "directory_id"
|
|
|
|
res_model = fields.Char(
|
|
string="Linked attachments model", index="btree", store=True
|
|
)
|
|
res_id = fields.Integer(
|
|
string="Linked attachments record ID", index="btree", store=True
|
|
)
|
|
record_ref = fields.Reference(
|
|
string="Record Referenced",
|
|
compute="_compute_record_ref",
|
|
selection=lambda self: self._get_ref_selection(),
|
|
)
|
|
permission_read = fields.Boolean(
|
|
compute="_compute_permissions",
|
|
search="_search_permission_read",
|
|
string="Read Access",
|
|
)
|
|
permission_create = fields.Boolean(
|
|
compute="_compute_permissions",
|
|
search="_search_permission_create",
|
|
string="Create Access",
|
|
)
|
|
permission_write = fields.Boolean(
|
|
compute="_compute_permissions",
|
|
search="_search_permission_write",
|
|
string="Write Access",
|
|
)
|
|
permission_unlink = fields.Boolean(
|
|
compute="_compute_permissions",
|
|
search="_search_permission_unlink",
|
|
string="Delete Access",
|
|
)
|
|
|
|
@api.model
|
|
def _get_ref_selection(self):
|
|
models = self.env["ir.model"].sudo().search([])
|
|
return [(model.model, model.name) for model in models]
|
|
|
|
@api.depends("res_model", "res_id")
|
|
def _compute_record_ref(self):
|
|
for record in self:
|
|
record.record_ref = False
|
|
if record.res_model and record.res_id:
|
|
record.record_ref = f"{record.res_model},{record.res_id}"
|
|
|
|
def _compute_permissions(self):
|
|
"""
|
|
Get permissions for the current record.
|
|
"""
|
|
|
|
# Update according to presence when applying ir.rule
|
|
self.invalidate_recordset()
|
|
if self.env.su:
|
|
self.update(
|
|
{
|
|
"permission_create": True,
|
|
"permission_read": True,
|
|
"permission_unlink": True,
|
|
"permission_write": True,
|
|
}
|
|
)
|
|
return
|
|
|
|
creatable = self._filter_access_rules("create")
|
|
readable = self._filter_access_rules("read")
|
|
unlinkable = self._filter_access_rules("unlink")
|
|
writeable = self._filter_access_rules("write")
|
|
for one in self:
|
|
one.update(
|
|
{
|
|
"permission_create": bool(one & creatable),
|
|
"permission_read": bool(one & readable),
|
|
"permission_unlink": bool(one & unlinkable),
|
|
"permission_write": bool(one & writeable),
|
|
}
|
|
)
|
|
|
|
@api.model
|
|
def _get_domain_by_inheritance(self, operation):
|
|
"""Get domain for inherited accessible records."""
|
|
if self.env.su:
|
|
return []
|
|
inherited_access_field = "storage_id_inherit_access_from_parent_record"
|
|
if self._name != "dms.directory":
|
|
inherited_access_field = f"{self._directory_field}.{inherited_access_field}"
|
|
inherited_access_domain = [
|
|
("storage_id_save_type", "=", "attachment"),
|
|
(inherited_access_field, "=", True),
|
|
]
|
|
domains = []
|
|
# Get all used related records
|
|
related_groups = self.sudo().read_group(
|
|
domain=inherited_access_domain + [("res_model", "!=", False)],
|
|
fields=["res_id:array_agg"],
|
|
groupby=["res_model"],
|
|
)
|
|
for group in related_groups:
|
|
try:
|
|
model = self.env[group["res_model"]]
|
|
except KeyError:
|
|
# The model might not be registered.
|
|
# This is normal if you are upgrading the database.
|
|
# Otherwise, you probably have garbage DMS data.
|
|
# These records will be accessible by DB users only.
|
|
domains.append(
|
|
[
|
|
("res_model", "=", group["res_model"]),
|
|
(True, "=", self.env.user.has_group("base.group_user")),
|
|
]
|
|
)
|
|
continue
|
|
# Check model access only once per batch
|
|
if not model.check_access_rights(operation, raise_exception=False):
|
|
continue
|
|
domains.append([("res_model", "=", model._name), ("res_id", "=", False)])
|
|
# Check record access in batch too
|
|
res_ids = [i for i in group["res_id"] if i] # Hack to remove None res_id
|
|
# Apply exists to skip records that do not exist. (e.g. a res.partner
|
|
# deleted by database).
|
|
model_records = model.browse(res_ids).exists()
|
|
related_ok = model_records._filter_access_rules_python(operation)
|
|
if not related_ok:
|
|
continue
|
|
domains.append(
|
|
[("res_model", "=", model._name), ("res_id", "in", related_ok.ids)]
|
|
)
|
|
result = inherited_access_domain + OR(domains)
|
|
return result
|
|
|
|
@api.model
|
|
def _get_access_groups_query(self, operation):
|
|
"""Return the query to select access groups."""
|
|
operation_check = {
|
|
"create": "AND dag.perm_inclusive_create",
|
|
"read": "",
|
|
"unlink": "AND dag.perm_inclusive_unlink",
|
|
"write": "AND dag.perm_inclusive_write",
|
|
}[operation]
|
|
select = f"""
|
|
SELECT
|
|
dir_group_rel.aid
|
|
FROM
|
|
dms_directory_complete_groups_rel AS dir_group_rel
|
|
INNER JOIN dms_access_group AS dag
|
|
ON dir_group_rel.gid = dag.id
|
|
INNER JOIN dms_access_group_users_rel AS users
|
|
ON users.gid = dag.id
|
|
WHERE
|
|
users.uid = %s {operation_check}
|
|
"""
|
|
return select, (self.env.uid,)
|
|
|
|
@api.model
|
|
def _get_domain_by_access_groups(self, operation):
|
|
"""Get domain for records accessible applying DMS access groups."""
|
|
result = [
|
|
(
|
|
"%s.storage_id_inherit_access_from_parent_record"
|
|
% self._directory_field,
|
|
"=",
|
|
False,
|
|
),
|
|
(
|
|
self._directory_field,
|
|
"inselect",
|
|
self._get_access_groups_query(operation),
|
|
),
|
|
]
|
|
return result
|
|
|
|
@api.model
|
|
def _get_permission_domain(self, operator, value, operation):
|
|
"""Abstract logic for searching computed permission fields."""
|
|
_self = self
|
|
# HACK ir.rule domain is always computed with sudo, so if this check is
|
|
# true, we can assume safely that you're checking permissions
|
|
if self.env.su and value == self.env.uid:
|
|
_self = self.sudo(False)
|
|
value = bool(value)
|
|
# Tricky one, to know if you want to search
|
|
# positive or negative access
|
|
positive = (operator not in NEGATIVE_TERM_OPERATORS) == bool(value)
|
|
if _self.env.su:
|
|
# You're SUPERUSER_ID
|
|
return TRUE_DOMAIN if positive else FALSE_DOMAIN
|
|
|
|
result = OR(
|
|
[
|
|
_self._get_domain_by_access_groups(operation),
|
|
_self._get_domain_by_inheritance(operation),
|
|
]
|
|
)
|
|
|
|
if not positive:
|
|
result.insert(0, "!")
|
|
return result
|
|
|
|
@api.model
|
|
def _search_permission_create(self, operator, value):
|
|
return self._get_permission_domain(operator, value, "create")
|
|
|
|
@api.model
|
|
def _search_permission_read(self, operator, value):
|
|
return self._get_permission_domain(operator, value, "read")
|
|
|
|
@api.model
|
|
def _search_permission_unlink(self, operator, value):
|
|
return self._get_permission_domain(operator, value, "unlink")
|
|
|
|
@api.model
|
|
def _search_permission_write(self, operator, value):
|
|
return self._get_permission_domain(operator, value, "write")
|
|
|
|
def _filter_access_rules_python(self, operation):
|
|
# Only kept to not break inheritance; see next comment
|
|
result = super()._filter_access_rules_python(operation)
|
|
# HACK Always fall back to applying rules by SQL.
|
|
# Upstream `_filter_access_rules_python()` doesn't use computed fields
|
|
# search methods. Thus, it will take the `[('permission_{operation}',
|
|
# '=', user.id)]` rule literally. Obviously that will always fail
|
|
# because `self[f"permission_{operation}"]` will always be a `bool`,
|
|
# while `user.id` will always be an `int`.
|
|
result |= self._filter_access_rules(operation)
|
|
return result
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
# Create as sudo to avoid testing creation permissions before DMS security
|
|
# groups are attached (otherwise nobody would be able to create)
|
|
res = super(DmsSecurityMixin, self.sudo()).create(vals_list)
|
|
# Need to flush now, so all groups are stored in DB and the SELECT used
|
|
# to check access works
|
|
res.flush_recordset()
|
|
# Go back to the original sudo state and check we really had creation permission
|
|
res = res.sudo(self.env.su)
|
|
res.check_access_rights("create")
|
|
res.check_access_rule("create")
|
|
return res
|