From be889efe45a6fae3bfc242b51a125e2712c832d7 Mon Sep 17 00:00:00 2001 From: Florian Charlier <flch@odoo.com> Date: Wed, 18 May 2022 09:07:03 +0000 Subject: [PATCH] [FIX] website_slides: fix resource ACLs This commit fixes ACLs for slide resources: * Access to resources should be blocked for non-members (except publishers for the course: responsible and managers) * Tests are included Task-2818136 Part of Task-2663320 Part-of: odoo/odoo#88454 --- addons/website_slides/models/slide_slide.py | 6 ++ .../security/ir.model.access.csv | 4 +- .../security/website_slides_security.xml | 35 +++++++++++- addons/website_slides/tests/test_security.py | 57 ++++++++++++++++++- .../views/website_slides_templates_lesson.xml | 15 ++--- ...ite_slides_templates_lesson_fullscreen.xml | 11 ++-- 6 files changed, 112 insertions(+), 16 deletions(-) diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index dc931ae91e7d..7b209064b93a 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -240,6 +240,12 @@ class Slide(models.Model): for slide in self: slide.questions_count = len(slide.question_ids) + def _has_additional_resources(self): + """Sudo required for public user to know if the course has additional + resources that they will be able to access once a member.""" + self.ensure_one() + return bool(self.sudo().slide_resource_ids) + @api.depends('website_message_ids.res_id', 'website_message_ids.model', 'website_message_ids.message_type') def _compute_comments_count(self): for slide in self: diff --git a/addons/website_slides/security/ir.model.access.csv b/addons/website_slides/security/ir.model.access.csv index 355ebc5ade44..aa3abd42fa67 100644 --- a/addons/website_slides/security/ir.model.access.csv +++ b/addons/website_slides/security/ir.model.access.csv @@ -23,7 +23,9 @@ access_slide_embed_all,slide.embed.all,model_slide_embed,,1,0,0,0 access_slide_embed_user,slide.embed.user,model_slide_embed,base.group_user,1,1,1,1 access_slide_slide_link_all,slide.slide.link.all,model_slide_slide_link,,1,0,0,0 access_slide_slide_link_officer,slide.slide.link.officer,model_slide_slide_link,website_slides.group_website_slides_officer,1,1,1,1 -access_slide_slide_resource_all,slide.slide.resource.all,model_slide_slide_resource,,1,0,0,0 +access_slide_slide_resource_all,slide.slide.resource.all,model_slide_slide_resource,,0,0,0,0 access_slide_slide_resource_public,slide.slide.resource.public,model_slide_slide_resource,base.group_public,0,0,0,0 +access_slide_slide_resource_portal,slide.slide.resource.portal,model_slide_slide_resource,base.group_portal,1,0,0,0 +access_slide_slide_resource_internal,slide.slide.resource.internal,model_slide_slide_resource,base.group_user,1,0,0,0 access_slide_slide_resource_publisher,slide.slide.resource.publisher,model_slide_slide_resource,website_slides.group_website_slides_officer,1,1,1,1 access_slide_channel_invite,access.slide.channel.invite,model_slide_channel_invite,base.group_user,1,1,1,0 diff --git a/addons/website_slides/security/website_slides_security.xml b/addons/website_slides/security/website_slides_security.xml index ab800865dd00..fad254027607 100644 --- a/addons/website_slides/security/website_slides_security.xml +++ b/addons/website_slides/security/website_slides_security.xml @@ -178,7 +178,7 @@ <!--SLIDE RESOURCE--> <record id="rule_slide_slide_resource_downloadable" model="ir.rule"> - <field name="name">Resource: restricted to channel members and channel responsible</field> + <field name="name">Resource: read restricted to channel members</field> <field name="model_id" ref="model_slide_slide_resource"/> <field name="domain_force">[('slide_id.channel_id.partner_ids', '=', user.partner_id.id)]</field> <field name="groups" eval="[(4, ref('base.group_portal')), (4, ref('base.group_user'))]"/> @@ -187,5 +187,38 @@ <field name="perm_create" eval="False"/> <field name="perm_unlink" eval="False"/> </record> + + <record id="rule_slide_slide_resource_officer_read" model="ir.rule"> + <field name="name">Resource: officer: read all</field> + <field name="model_id" ref="model_slide_slide_resource"/> + <field name="domain_force">[(1, '=', 1)]</field> + <field name="groups" eval="[(4, ref('group_website_slides_officer'))]"/> + <field name="perm_read" eval="True"/> + <field name="perm_write" eval="False"/> + <field name="perm_create" eval="False"/> + <field name="perm_unlink" eval="False"/> + </record> + + <record id="rule_slide_slide_resource_officer_crud" model="ir.rule"> + <field name="name">Resource: officer: crud own only</field> + <field name="model_id" ref="model_slide_slide_resource"/> + <field name="domain_force">[('slide_id.channel_id.user_id', '=', user.id)]</field> + <field name="groups" eval="[(4, ref('group_website_slides_officer'))]"/> + <field name="perm_read" eval="True"/> + <field name="perm_write" eval="True"/> + <field name="perm_create" eval="True"/> + <field name="perm_unlink" eval="True"/> + </record> + + <record id="rule_slide_slide_resource_manager" model="ir.rule"> + <field name="name">Resource: manager: crud all</field> + <field name="model_id" ref="model_slide_slide_resource"/> + <field name="domain_force">[(1, '=', 1)]</field> + <field name="groups" eval="[(4, ref('group_website_slides_manager'))]"/> + <field name="perm_read" eval="True"/> + <field name="perm_write" eval="True"/> + <field name="perm_create" eval="True"/> + <field name="perm_unlink" eval="True"/> + </record> </data> </odoo> diff --git a/addons/website_slides/tests/test_security.py b/addons/website_slides/tests/test_security.py index 1f5eecc31436..9ffdccba6c66 100644 --- a/addons/website_slides/tests/test_security.py +++ b/addons/website_slides/tests/test_security.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import base64 +from odoo.addons.mail.tests.common import mail_new_test_user from odoo.addons.website_slides.tests import common -from odoo.exceptions import AccessError, UserError +from odoo.exceptions import AccessError from odoo.tests import tagged -from odoo.tests.common import users from odoo.tools import mute_logger @@ -298,3 +299,55 @@ class TestAccessFeatures(common.SlidesCase): channel_superuser.invalidate_cache(['can_upload', 'can_publish']) self.assertTrue(channel_superuser.can_upload) self.assertTrue(channel_superuser.can_publish) + + @mute_logger('odoo.models.unlink', 'odoo.addons.base.models.ir_rule', 'odoo.addons.base.models.ir_model') + def test_resource_access(self): + resource_values = { + 'name': 'Image', + 'slide_id': self.slide_3.id, + 'data': base64.b64encode(b'Some content') + } + resource1, resource2 = self.env['slide.slide.resource'].with_user(self.user_officer).create( + [resource_values for _ in range(2)]) + + # No public access + with self.assertRaises(AccessError): + resource1.with_user(self.user_public).read(['name']) + with self.assertRaises(AccessError): + resource1.with_user(self.user_public).write({'name': 'other name'}) + + # No random portal access + with self.assertRaises(AccessError): + resource1.with_user(self.user_portal).read(['name']) + + # Members can only read + self.env['slide.channel.partner'].create({ + 'channel_id': self.channel.id, + 'partner_id': self.user_portal.partner_id.id, + }) + resource1.with_user(self.user_portal).read(['name']) + with self.assertRaises(AccessError): + resource1.with_user(self.user_portal).write({'name': 'other name'}) + + # Other officers can only read + user_officer_other = mail_new_test_user( + self.env, name='Ornella Officer', login='user_officer_2', email='officer2@example.com', + groups='base.group_user,website_slides.group_website_slides_officer' + ) + resource1.with_user(user_officer_other).read(['name']) + with self.assertRaises(AccessError): + resource1.with_user(user_officer_other).write({'name': 'Another name'}) + + with self.assertRaises(AccessError): + self.env['slide.slide.resource'].with_user(user_officer_other).create(resource_values) + with self.assertRaises(AccessError): + resource1.with_user(user_officer_other).unlink() + + # Responsible officer can do anything on their own channels + resource1.with_user(self.user_officer).write({'name': 'other name'}) + resource1.with_user(self.user_officer).unlink() + + # Managers can do anything on all channels + resource2.with_user(self.user_manager).write({'name': 'Another name'}) + resource2.with_user(self.user_manager).unlink() + self.env['slide.slide.resource'].with_user(self.user_manager).create(resource_values) diff --git a/addons/website_slides/views/website_slides_templates_lesson.xml b/addons/website_slides/views/website_slides_templates_lesson.xml index a8e9ad2a662e..8fd7575d1c86 100644 --- a/addons/website_slides/views/website_slides_templates_lesson.xml +++ b/addons/website_slides/views/website_slides_templates_lesson.xml @@ -138,10 +138,11 @@ </t> </a> <ul class="collapse show p-0 m-0 list-unstyled" t-att-id="('collapse-%s') % (category.id if category else 0)" > + <t t-set="is_member" t-value="slide.channel_id.is_member"/> + <t t-set="can_access_channel" t-value="is_member or slide.channel_id.can_publish"/> <t t-foreach="category_slide_ids" t-as="aside_slide"> <t t-set="slide_completed" t-value="channel_progress[aside_slide.id].get('completed')"/> - <t t-set="is_member" t-value="slide.channel_id.is_member"/> - <t t-set="can_access" t-value="aside_slide.is_preview or is_member or slide.channel_id.can_publish"/> + <t t-set="can_access" t-value="can_access_channel or aside_slide.is_preview"/> <li class="p-0 pb-1"> <a t-att-href="'/slides/slide/%s' % (slug(aside_slide)) if can_access else '#'" t-att-class="'o_wslides_lesson_aside_list_link d-flex align-items-top px-2 pt-1 text-decoration-none %s%s' % (('bg-100 py-1 active' if aside_slide == slide else ''), 'text-muted' if not can_access else '')"> @@ -162,10 +163,10 @@ </span> </div> </a> - <ul t-if="aside_slide.link_ids or aside_slide.slide_resource_ids or aside_slide.question_ids" class="list-group px-2 mb-1 list-unstyled"> + <ul t-if="aside_slide.link_ids or aside_slide._has_additional_resources() or aside_slide.question_ids" class="list-group px-2 mb-1 list-unstyled"> <t t-foreach="aside_slide.link_ids" t-as="resource"> <li class="pl-4"> - <a t-if="can_access" t-att-href="resource.link" target="new" class="text-decoration-none small"> + <a t-if="can_access_channel" t-att-href="resource.link" target="new" class="text-decoration-none small"> <i class="fa fa-link mr-1"/><span t-field="resource.name"/> </a> <span t-else="" class="text-decoration-none text-muted small"> @@ -173,8 +174,8 @@ </span> </li> </t> - <div class="o_wslides_js_course_join pl-4" t-if="aside_slide.slide_resource_ids"> - <t t-if="is_member or aside_slide.channel_id.can_publish"> + <div class="o_wslides_js_course_join pl-4" t-if="aside_slide._has_additional_resources()"> + <t t-if="can_access_channel"> <li t-foreach="aside_slide.slide_resource_ids" t-as="resource"> <a t-attf-href="/web/content/slide.slide.resource/#{resource.id}/data?download=true" class="text-decoration-none small"> <i class="fa fa-download mr-1"/><span t-field="resource.name"/> @@ -403,7 +404,7 @@ </t> </div> </div> - <div class="col-12 col-md d-flex align-items-start mb-4 mb-md-0 o_wslides_js_course_join" t-if="len(slide.slide_resource_ids)"> + <div class="col-12 col-md d-flex align-items-start mb-4 mb-md-0 o_wslides_js_course_join" t-if="slide._has_additional_resources()"> <span t-if="slide.channel_id.is_member or slide.channel_id.can_publish or slide.is_preview or slide.channel_id.enroll in ['private', 'payment']" class="text-muted font-weight-bold mr-3"> Additional Resources </span> diff --git a/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml b/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml index 2fa732b2ac5e..3d8d405908e7 100644 --- a/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml +++ b/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml @@ -84,10 +84,11 @@ <b t-field="category.name"/> </a> <ul class="o_wslides_fs_sidebar_section_slides collapse show position-relative px-0 pb-1 my-0 mx-n3" t-att-id="('collapse-%s') % (category.id if category else 0)"> + <t t-set="is_member" t-value="current_slide.channel_id.is_member"/> + <t t-set="can_access_channel" t-value="is_member or current_slide.channel_id.can_publish"/> <t t-foreach="slides" t-as="slide"> <t t-set="slide_completed" t-value="channel_progress[slide.id].get('completed')"/> - <t t-set="is_member" t-value="current_slide.channel_id.is_member"/> - <t t-set="can_access" t-value="slide.is_preview or is_member or current_slide.channel_id.can_publish"/> + <t t-set="can_access" t-value="can_access_channel or slide.is_preview"/> <li t-att-class="'o_wslides_fs_sidebar_list_item d-flex align-items-top py-1 %s' % ('active' if slide.id == current_slide.id else '')" t-att-data-id="slide.id" t-att-data-can-access="can_access" @@ -118,7 +119,7 @@ <div class="o_wslides_fs_slide_name text-600" t-esc="slide.name"/> </div> </span> - <ul class="list-unstyled w-100 pt-2 small" t-if="slide.link_ids or slide.slide_resource_ids or (slide.question_ids and not slide.slide_type =='quiz')" > + <ul class="list-unstyled w-100 pt-2 small" t-if="slide.link_ids or slide._has_additional_resources() or (slide.question_ids and not slide.slide_type =='quiz')" > <li t-if="slide.link_ids" t-foreach="slide.link_ids" t-as="link" class="pl-0 mb-1"> <a t-if="can_access" class="o_wslides_fs_slide_link" t-att-href="link.link" target="_blank"> <i class="fa fa-link mr-2"/><span t-esc="link.name"/> @@ -127,8 +128,8 @@ <i class="fa fa-link mr-2"/><span t-esc="link.name"/> </span> </li> - <div class="o_wslides_js_course_join pl-0" t-if="slide.slide_resource_ids"> - <t t-if="is_member or slide.channel_id.can_publish"> + <div class="o_wslides_js_course_join pl-0" t-if="slide._has_additional_resources()"> + <t t-if="can_access_channel"> <li t-foreach="slide.slide_resource_ids" t-as="resource" class="mb-1"> <a class="o_wslides_fs_slide_link" t-attf-href="/web/content/slide.slide.resource/#{resource.id}/data?download=true"> <i class="fa fa-download mr-2"/><span t-esc="resource.name"/> -- GitLab