def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): """Override read_group to add res_field=False in domain if not present.""" if not fields: raise AccessError( _("Sorry, you must provide fields to read on attachments")) if any('(' in field for field in fields + groupby): raise AccessError( _("Sorry, the syntax 'name:agg(field)' is not available for attachments" )) if not any(item[0] in ('id', 'res_field') for item in domain): domain.insert(0, ('res_field', '=', False)) groupby = [groupby] if isinstance(groupby, str) else groupby allowed_fields = self._read_group_allowed_fields() fields_set = set(field.split(':')[0] for field in fields + groupby) if not self.env.is_system() and ( not fields or fields_set.difference(allowed_fields)): raise AccessError( _("Sorry, you are not allowed to access these fields on attachments." )) return super().read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
def _make_access_error(self, operation, records): _logger.info( 'Access Denied by record rules for operation: %s on record ids: %r, uid: %s, model: %s', operation, records.ids[:6], self._uid, records._name) model = records._name description = self.env['ir.model']._get(model).name or model if not self.env.user.has_group('base.group_no_one'): return AccessError( _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: "%(document_kind)s" (%(document_model)s), Operation: %(operation)s)' ) % { 'document_kind': description, 'document_model': model, 'operation': operation, }) # This extended AccessError is only displayed in debug mode. # Note that by default, public and portal users do not have # the group "base.group_no_one", even if debug mode is enabled, # so it is relatively safe here to include the list of rules and # record names. rules = self._get_failing(records, mode=operation).sudo() error = AccessError( _("""The requested operation ("%(operation)s" on "%(document_kind)s" (%(document_model)s)) was rejected because of the following rules: %(rules_list)s %(multi_company_warning)s (Records: %(example_records)s, User: %(user_id)s)""") % { 'operation': operation, 'document_kind': description, 'document_model': model, 'rules_list': '\n'.join('- %s' % rule.name for rule in rules), 'multi_company_warning': ('\n' + _('Note: this might be a multi-company issue.') + '\n') if any('company_id' in (r.domain_force or []) for r in rules) else '', 'example_records': ' - '.join([ '%s (id=%s)' % (rec.display_name, rec.id) for rec in records[:6].sudo() ]), 'user_id': '%s (id=%s)' % (self.env.user.name, self.env.user.id), }) # clean up the cache of records prefetched with display_name above for record in records[:6]: record._cache.clear() return error
def _read(self, fields): # DLE P45: `test_31_prefetch`, # with self.assertRaises(AccessError): # cat1.name if self.search_count([('id', 'in', self._ids), ('name', '=', 'NOACCESS')]): raise AccessError('Sorry') return super(Category, self)._read(fields)
def read(self, fields, load='_classic_read'): if self.check_access_rights('read', raise_exception=False): return super(HrEmployeePrivate, self).read(fields, load=load) private_fields = set(fields).difference(self.env['hr.employee.public']._fields.keys()) if private_fields: raise AccessError(_('The fields "%s" you try to read is not available on the public employee profile.') % (','.join(private_fields))) return self.env['hr.employee.public'].browse(self.ids).read(fields, load=load)
def convert_answer_to_comment(self): """ Tools to convert an answer (forum.post) to a comment (mail.message). The original post is unlinked and a new comment is posted on the question using the post create_uid as the comment's author. """ self.ensure_one() if not self.parent_id: return self.env['mail.message'] # karma-based action check: use the post field that computed own/all value if not self.can_comment_convert: raise AccessError(_('%d karma required to convert an answer to a comment.') % self.karma_comment_convert) # post the message question = self.parent_id self_sudo = self.sudo() values = { 'author_id': self_sudo.create_uid.partner_id.id, # use sudo here because of access to res.users model 'email_from': self_sudo.create_uid.email_formatted, # use sudo here because of access to res.users model 'body': tools.html_sanitize(self.content, sanitize_attributes=True, strip_style=True, strip_classes=True), 'message_type': 'comment', 'subtype': 'mail.mt_comment', 'date': self.create_date, } # done with the author user to have create_uid correctly set new_message = question.with_user(self_sudo.create_uid.id).with_context(mail_create_nosubscribe=True).message_post(**values) # unlink the original answer, using SUPERUSER_ID to avoid karma issues self.sudo().unlink() return new_message
def migrate_to_lobject(self): """migrates all binary attachments to postgres large object storage""" if not self.env.user._is_admin(): raise AccessError( _('Only administrators can execute this action.')) atts = self.search([ '&', '&', ('id', '>', 0), # bypass filtering of field-attached attachments ('type', '=', 'binary'), '|', ('store_fname', '=', False), ('store_fname', 'not like', LARGE_OBJECT_LOCATION + ':%') ]) att_count = len(atts) if att_count: log.info( f'Migrating {att_count} database attachments to Large Objects...' ) if self._storage() != LARGE_OBJECT_LOCATION: raise Exception( f'Default storage is not set to Large Object ({LARGE_OBJECT_LOCATION})' ) current_att = 1 for att in atts: log.info( f'Migrating attachment ID {att.id} ({current_att} of {att_count})...' ) # re-save data to move to lobject storage att.write({'mimetype': att.mimetype, 'datas': att.datas}) current_att += 1
def company(self): """Return the current company (as an instance). If not specified in the context (`allowed_company_ids`), fallback on current user main company. :raise AccessError: invalid or unauthorized `allowed_company_ids` context key content. :return: current company (default=`self.user.company_id`) :rtype: res.company .. warning:: No sanity checks applied in sudo mode ! When in sudo mode, a user can access any company, even if not in his allowed companies. This allows to trigger inter-company modifications, even if the current user doesn't have access to the targeted company. """ company_ids = self.context.get('allowed_company_ids', []) if company_ids: if not self.su: user_company_ids = self.user.company_ids.ids if any(cid not in user_company_ids for cid in company_ids): raise AccessError(_("Access to unauthorized or invalid companies.")) return self['res.company'].browse(company_ids[0]) return self.user.company_id
def get_mail_message_access(self, res_ids, operation, model_name=None): # XDO FIXME: to be correctly fixed with new get_mail_message_access and filter access rule if operation in ('write', 'unlink') and (not model_name or model_name == 'forum.post'): # Make sure only author or moderator can edit/delete messages for post in self.browse(res_ids): if not post.can_edit: raise AccessError(_('%d karma required to edit a post.') % post.karma_edit) return super(Post, self).get_mail_message_access(res_ids, operation, model_name=model_name)
def unlink(self): for post in self: if not post.can_unlink: raise AccessError(_('%d karma required to unlink a post.') % post.karma_unlink) # if unlinking an answer with accepted answer: remove provided karma for post in self: if post.is_correct: post.create_uid.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * -1) self.env.user.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * -1) return super(Post, self).unlink()
def convert_comment_to_answer(self, message_id, default=None): """ Tool to convert a comment (mail.message) into an answer (forum.post). The original comment is unlinked and a new answer from the comment's author is created. Nothing is done if the comment's author already answered the question. """ comment = self.env['mail.message'].sudo().browse(message_id) post = self.browse(comment.res_id) if not comment.author_id or not comment.author_id.user_ids: # only comment posted by users can be converted return False # karma-based action check: must check the message's author to know if own / all is_author = comment.author_id.id == self.env.user.partner_id.id karma_own = post.forum_id.karma_comment_convert_own karma_all = post.forum_id.karma_comment_convert_all karma_convert = is_author and karma_own or karma_all can_convert = self.env.user.karma >= karma_convert if not can_convert: if is_author and karma_own < karma_all: raise AccessError(_('%d karma required to convert your comment to an answer.') % karma_own) else: raise AccessError(_('%d karma required to convert a comment to an answer.') % karma_all) # check the message's author has not already an answer question = post.parent_id if post.parent_id else post post_create_uid = comment.author_id.user_ids[0] if any(answer.create_uid.id == post_create_uid.id for answer in question.child_ids): return False # create the new post post_values = { 'forum_id': question.forum_id.id, 'content': comment.body, 'parent_id': question.id, 'name': _('Re: %s') % (question.name or ''), } # done with the author user to have create_uid correctly set new_post = self.with_user(post_create_uid).create(post_values) # delete comment comment.unlink() return new_post
def _compute_kpi_hr_recruitment_new_colleagues_value(self): if not self.env.user.has_group('hr_recruitment.group_hr_recruitment_user'): raise AccessError(_("Do not have access, skip this data for user's digest email")) for record in self: start, end, company = record._get_kpi_compute_parameters() new_colleagues = self.env['hr.employee'].search_count([ ('create_date', '>=', start), ('create_date', '<', end), ('company_id', '=', company.id) ]) record.kpi_hr_recruitment_new_colleagues_value = new_colleagues
def create(self, vals_list): records = super(WebsitePublishedMixin, self).create(vals_list) is_publish_modified = any([ set(v.keys()) & {'is_published', 'website_published'} for v in vals_list ]) if is_publish_modified and not all(record.can_publish for record in records): raise AccessError(self._get_can_publish_error_message()) return records
def _compute_project_task_opened_value(self): if not self.env.user.has_group('project.group_project_user'): raise AccessError(_("Do not have access, skip this data for user's digest email")) for record in self: start, end, company = record._get_kpi_compute_parameters() record.kpi_project_task_opened_value = self.env['project.task'].search_count([ ('stage_id.fold', '=', False), ('create_date', '>=', start), ('create_date', '<', end), ('company_id', '=', company.id) ])
def _compute_kpi_crm_lead_created_value(self): if not self.env.user.has_group('sales_team.group_sale_salesman'): raise AccessError( _("Do not have access, skip this data for user's digest email") ) for record in self: start, end, company = record._get_kpi_compute_parameters() record.kpi_crm_lead_created_value = self.env[ 'crm.lead'].search_count([('create_date', '>=', start), ('create_date', '<', end), ('company_id', '=', company.id)])
def create(self, vals): if 'content' in vals and vals.get('forum_id'): vals['content'] = self._update_content(vals['content'], vals['forum_id']) post = super(Post, self.with_context(mail_create_nolog=True)).create(vals) # deleted or closed questions if post.parent_id and (post.parent_id.state == 'close' or post.parent_id.active is False): raise UserError(_('Posting answer on a [Deleted] or [Closed] question is not possible.')) # karma-based access if not post.parent_id and not post.can_ask: raise AccessError(_('%d karma required to create a new question.') % post.forum_id.karma_ask) elif post.parent_id and not post.can_answer: raise AccessError(_('%d karma required to answer a question.') % post.forum_id.karma_answer) if not post.parent_id and not post.can_post: post.sudo().state = 'pending' # add karma for posting new questions if not post.parent_id and post.state == 'active': self.env.user.sudo().add_karma(post.forum_id.karma_gen_question_new) post.post_notification() return post
def _compute_kpi_pos_total_value(self): if not self.env.user.has_group('point_of_sale.group_pos_user'): raise AccessError( _("Do not have access, skip this data for user's digest email") ) for record in self: start, end, company = record._get_kpi_compute_parameters() record.kpi_pos_total_value = sum(self.env['pos.order'].search([ ('date_order', '>=', start), ('date_order', '<', end), ('state', 'not in', ['draft', 'cancel', 'invoiced']), ('company_id', '=', company.id) ]).mapped('amount_total'))
def _parse_import_data_recursive(self, model, prefix, data, import_fields, options): # Get fields of type date/datetime all_fields = self.env[model].fields_get() for name, field in all_fields.items(): name = prefix + name if field['type'] in ('date', 'datetime') and name in import_fields: index = import_fields.index(name) self._parse_date_from_data(data, index, name, field['type'], options) # Check if the field is in import_field and is a relational (followed by /) # Also verify that the field name exactly match the import_field at the correct level. elif any(name + '/' in import_field and name == import_field.split('/')[prefix.count('/')] for import_field in import_fields): # Recursive call with the relational as new model and add the field name to the prefix self._parse_import_data_recursive(field['relation'], name + '/', data, import_fields, options) elif field['type'] in ('float', 'monetary') and name in import_fields: # Parse float, sometimes float values from file have currency symbol or () to denote a negative value # We should be able to manage both case index = import_fields.index(name) self._parse_float_from_data(data, index, name, options) elif field['type'] == 'binary' and field.get('attachment') and any( f in name for f in IMAGE_FIELDS) and name in import_fields: index = import_fields.index(name) with requests.Session() as session: session.stream = True for num, line in enumerate(data): if re.match( config.get("import_image_regex", DEFAULT_IMAGE_REGEX), line[index]): if not self.env.user._can_import_remote_urls(): raise AccessError( _("You can not import images via URL, check with your administrator or support for the reason." )) line[index] = self._import_image_by_url( line[index], session, name, num) else: try: base64.b64decode(line[index], validate=True) except binascii.Error: raise ValueError( _("Found invalid image data, images should be imported as either URLs or base64-encoded data." )) return data
def _update_content(self, content, forum_id): forum = self.env['forum.forum'].browse(forum_id) if content and self.env.user.karma < forum.karma_dofollow: for match in re.findall(r'<a\s.*href=".*?">', content): match = re.escape(match) # replace parenthesis or special char in regex content = re.sub(match, match[:3] + 'rel="nofollow" ' + match[3:], content) if self.env.user.karma <= forum.karma_editor: filter_regexp = r'(<img.*?>)|(<a[^>]*?href[^>]*?>)|(<[a-z|A-Z]+[^>]*style\s*=\s*[\'"][^\'"]*\s*background[^:]*:[^url;]*url)' content_match = re.search(filter_regexp, content, re.I) if content_match: raise AccessError(_('%d karma required to post an image or link.') % forum.karma_editor) return content
def _compute_kpi_crm_opportunities_won_value(self): if not self.env.user.has_group('sales_team.group_sale_salesman'): raise AccessError( _("Do not have access, skip this data for user's digest email") ) for record in self: start, end, company = record._get_kpi_compute_parameters() record.kpi_crm_opportunities_won_value = self.env[ 'crm.lead'].search_count([('type', '=', 'opportunity'), ('probability', '=', '100'), ('date_closed', '>=', start), ('date_closed', '<', end), ('company_id', '=', company.id)])
def run(self): """ Runs the server action. For each server action, the run_action_<STATE> method is called. This allows easy overriding of the server actions. :param dict context: context should contain following keys - active_id: id of the current object (single mode) - active_model: current model that should equal the action's model The following keys are optional: - active_ids: ids of the current records (mass mode). If active_ids and active_id are present, active_ids is given precedence. :return: an action_id to be executed, or False is finished correctly without return action """ res = False for action in self: action_groups = action.groups_id if action_groups and not (action_groups & self.env.user.groups_id): raise AccessError( _("You don't have enough access rights to run this action." )) eval_context = self._get_eval_context(action) if hasattr(self, 'run_action_%s_multi' % action.state): # call the multi method run_self = self.with_context(eval_context['env'].context) func = getattr(run_self, 'run_action_%s_multi' % action.state) res = func(action, eval_context=eval_context) elif hasattr(self, 'run_action_%s' % action.state): active_id = self._context.get('active_id') if not active_id and self._context.get('onchange_self'): active_id = self._context['onchange_self']._origin.id if not active_id: # onchange on new record func = getattr(self, 'run_action_%s' % action.state) res = func(action, eval_context=eval_context) active_ids = self._context.get( 'active_ids', [active_id] if active_id else []) for active_id in active_ids: # run context dedicated to a particular active_id run_self = self.with_context(active_ids=[active_id], active_id=active_id) eval_context["env"].context = run_self._context # call the single method related to the action: run_action_<STATE> func = getattr(run_self, 'run_action_%s' % action.state) res = func(action, eval_context=eval_context) return res or False
def validate(self): for post in self: if not post.can_moderate: raise AccessError(_('%d karma required to validate a post.') % post.forum_id.karma_moderate) # if state == pending, no karma previously added for the new question if post.state == 'pending': post.create_uid.sudo().add_karma(post.forum_id.karma_gen_question_new) post.write({ 'state': 'active', 'active': True, 'moderator_id': self.env.user.id, }) post.post_notification() return True
def force_storage(self): """Force all attachments to be stored in the currently configured storage""" if not self.env.is_admin(): raise AccessError( _('Only administrators can execute this action.')) # domain to retrieve the attachments to migrate domain = { 'db': [('store_fname', '!=', False)], 'file': [('db_datas', '!=', False)], }[self._storage()] for attach in self.search(domain): attach.write({'datas': attach.datas}) return True
def _compute_kpi_website_sale_total_value(self): if not self.env.user.has_group( 'sales_team.group_sale_salesman_all_leads'): raise AccessError( _("Do not have access, skip this data for user's digest email") ) for record in self: start, end, company = record._get_kpi_compute_parameters() confirmed_website_sales = self.env['sale.order'].search([ ('date_order', '>=', start), ('date_order', '<', end), ('state', 'not in', ['draft', 'cancel', 'sent']), ('website_id', '!=', False), ('company_id', '=', company.id) ]) record.kpi_website_sale_total_value = sum( confirmed_website_sales.mapped('amount_total'))
def check_access_rule(self, operation): """ Add Access rules of mail.message for non-employee user: - read: - raise if the type is comment and subtype NULL (internal note) """ if self.user_has_groups('base.group_public'): self.env.cr.execute( 'SELECT id FROM "%s" WHERE website_published IS NOT TRUE AND id = ANY (%%s)' % (self._table), (self.ids, )) if self.env.cr.fetchall(): raise AccessError( _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)' ) % (self._description, operation) + ' - ({} {}, {} {})'.format(_('Records:'), self.ids[:6], _('User:'), self._uid)) return super(MailMessage, self).check_access_rule(operation=operation)
def mark_as_offensive(self, reason_id): for post in self: if not post.can_moderate: raise AccessError(_('%d karma required to mark a post as offensive.') % post.forum_id.karma_moderate) # remove some karma _logger.info('Downvoting user <%s> for posting spam/offensive contents', post.create_uid) post.create_uid.sudo().add_karma(post.forum_id.karma_gen_answer_flagged) # TODO: potential bottleneck, could be done in batch post.write({ 'state': 'offensive', 'moderator_id': self.env.user.id, 'closed_date': fields.Datetime.now(), 'closed_reason_id': reason_id, 'active': False, }) return True
def write(self, vals): """ Synchronize user and its related employee and check access rights if employees are not allowed to update their own data (otherwise sudo is applied for self data). """ hr_fields = { field for field_name, field in self._fields.items() if field.related_field and field.related_field.model_name == 'hr.employee' and field_name in vals } can_edit_self = self.env['ir.config_parameter'].sudo().get_param( 'hr.hr_employee_self_edit') or self.env.user.has_group( 'hr.group_hr_user') if hr_fields and not can_edit_self: # Raise meaningful error message raise AccessError( _("You are only allowed to update your preferences. Please contact a HR officer to update other informations." )) result = super(User, self).write(vals) employee_values = {} for fname in [ f for f in ['name', 'email', 'image_1920', 'tz'] if f in vals ]: employee_values[fname] = vals[fname] if employee_values: if 'email' in employee_values: employee_values['work_email'] = employee_values.pop('email') if 'image_1920' in vals: without_image = self.env['hr.employee'].sudo().search([ ('user_id', 'in', self.ids), ('image_1920', '=', False) ]) with_image = self.env['hr.employee'].sudo().search([ ('user_id', 'in', self.ids), ('image_1920', '!=', False) ]) without_image.write(employee_values) if not can_edit_self: employee_values.pop('image_1920') with_image.write(employee_values) else: self.env['hr.employee'].sudo().search([ ('user_id', 'in', self.ids) ]).write(employee_values) return result
def message_post(self, *, parent_id=False, subtype=None, **kwargs): """ Temporary workaround to avoid spam. If someone replies on a channel through the 'Presentation Published' email, it should be considered as a note as we don't want all channel followers to be notified of this answer. """ self.ensure_one() if kwargs.get('message_type') == 'comment' and not self.can_review: raise AccessError(_('Not enough karma to review')) if parent_id: parent_message = self.env['mail.message'].sudo().browse(parent_id) if parent_message.subtype_id and parent_message.subtype_id == self.env.ref( 'website_slides.mt_channel_slide_published'): if kwargs.get('subtype_id'): kwargs['subtype_id'] = False subtype = 'mail.mt_note' return super(Channel, self).message_post(parent_id=parent_id, subtype=subtype, **kwargs)
def _compute_kpi_sale_total_value(self): if not self.env.user.has_group( 'sales_team.group_sale_salesman_all_leads'): raise AccessError( _("Do not have access, skip this data for user's digest email") ) for record in self: start, end, company = record._get_kpi_compute_parameters() all_channels_sales = self.env['sale.report'].read_group( [('date', '>=', start), ('date', '<', end), ('state', 'not in', ['draft', 'cancel', 'sent']), ('company_id', '=', company.id)], ['price_total'], ['price_total']) record.kpi_all_sale_total_value = sum([ channel_sale['price_total'] for channel_sale in all_channels_sales ])
def write(self, vals): trusted_keys = ['active', 'is_correct', 'tag_ids'] # fields where security is checked manually if 'content' in vals: vals['content'] = self._update_content(vals['content'], self.forum_id.id) tag_ids = False if 'tag_ids' in vals: tag_ids = set(tag.get('id') for tag in self.resolve_2many_commands('tag_ids', vals['tag_ids'])) for post in self: if 'state' in vals: if vals['state'] in ['active', 'close']: if not post.can_close: raise AccessError(_('%d karma required to close or reopen a post.') % post.karma_close) trusted_keys += ['state', 'closed_uid', 'closed_date', 'closed_reason_id'] elif vals['state'] == 'flagged': if not post.can_flag: raise AccessError(_('%d karma required to flag a post.') % post.forum_id.karma_flag) trusted_keys += ['state', 'flag_user_id'] if 'active' in vals: if not post.can_unlink: raise AccessError(_('%d karma required to delete or reactivate a post.') % post.karma_unlink) if 'is_correct' in vals: if not post.can_accept: raise AccessError(_('%d karma required to accept or refuse an answer.') % post.karma_accept) # update karma except for self-acceptance mult = 1 if vals['is_correct'] else -1 if vals['is_correct'] != post.is_correct and post.create_uid.id != self._uid: post.create_uid.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * mult) self.env.user.sudo().add_karma(post.forum_id.karma_gen_answer_accept * mult) if tag_ids: if set(post.tag_ids.ids) != tag_ids and self.env.user.karma < post.forum_id.karma_edit_retag: raise AccessError(_('%d karma required to retag.') % post.forum_id.karma_edit_retag) if any(key not in trusted_keys for key in vals) and not post.can_edit: raise AccessError(_('%d karma required to edit a post.') % post.karma_edit) res = super(Post, self).write(vals) # if post content modify, notify followers if 'content' in vals or 'name' in vals: for post in self: if post.parent_id: body, subtype = _('Answer Edited'), 'website_forum.mt_answer_edit' obj_id = post.parent_id else: body, subtype = _('Question Edited'), 'website_forum.mt_question_edit' obj_id = post obj_id.message_post(body=body, subtype=subtype) if 'active' in vals: answers = self.env['forum.post'].with_context(active_test=False).search([('parent_id', 'in', self.ids)]) if answers: answers.write({'active': vals['active']}) return res
def execute(self): self.ensure_one() if not self.env.is_admin(): raise AccessError(_("Only administrators can change the settings")) self = self.with_context(active_test=False) classified = self._get_classified_fields() self.set_values() # module fields: install/uninstall the selected modules to_install = [] to_uninstall_modules = self.env['ir.module.module'] lm = len('module_') for name, module in classified['module']: if int(self[name]): to_install.append((name[lm:], module)) else: if module and module.state in ('installed', 'to upgrade'): to_uninstall_modules += module if to_install or to_uninstall_modules: self.flush() if to_uninstall_modules: to_uninstall_modules.button_immediate_uninstall() result = self._install_modules(to_install) if result or to_uninstall_modules: # After the uninstall/install calls, the registry and environments # are no longer valid. So we reset the environment. self.env.reset() self = self.env()[self._name] # pylint: disable=next-method-called config = self.env['res.config'].next() or {} if config.get('type') not in ('ir.actions.act_window_close', ): return config # force client-side reload (update user menu and current view) return { 'type': 'ir.actions.client', 'tag': 'reload', }