[IMP] payment,*: restrict access to payment.transaction records

Since 15.0, it is no longer possible to have partnerless transactions.
Nonetheless, migrated databases could contain transactions without a
`partner_id` set.
This commit cleans existing rules to make those transactions are not
accessible to unwanted users.

We take the opportunity to clean the security of payment.transaction
records globally.
Now only admin and accounting users have access to payment.transaction
records. The code of different applications has been adapted accordingly.

Task - 3102824

closes odoo/odoo#113515

Related: odoo/upgrade#4564
Related: odoo/enterprise#40939
Signed-off-by: Masereel Pierre <pim@odoo.com>
This commit is contained in:
Horacio Tellez
2023-05-15 15:07:13 +02:00
committed by Horacio Tellez (hote)
parent d471391dd8
commit 2b1e2abda3
14 changed files with 58 additions and 74 deletions
@@ -15,7 +15,8 @@ class AccountMove(models.Model):
readonly=True, copy=False)
authorized_transaction_ids = fields.Many2many(
string="Authorized Transactions", comodel_name='payment.transaction',
compute='_compute_authorized_transaction_ids', readonly=True, copy=False)
compute='_compute_authorized_transaction_ids', readonly=True, copy=False,
compute_sudo=True)
amount_paid = fields.Monetary(
string="Amount paid",
compute='_compute_amount_paid'
@@ -64,7 +65,7 @@ class AccountMove(models.Model):
def get_portal_last_transaction(self):
self.ensure_one()
return self.with_context(active_test=False).transaction_ids._get_last()
return self.with_context(active_test=False).transaction_ids.sudo()._get_last()
def payment_action_capture(self):
""" Capture all transactions linked to this invoice. """
@@ -1,3 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
payment_link_wizard,payment.link.wizard,payment.model_payment_link_wizard,account.group_account_invoice,1,1,1,0
payment_refund_wizard,payment.refund.wizard,model_payment_refund_wizard,account.group_account_invoice,1,1,1,0
payment_transaction_user,payment.transaction.user,model_payment_transaction,account.group_account_invoice,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 payment_link_wizard payment.link.wizard payment.model_payment_link_wizard account.group_account_invoice 1 1 1 0
3 payment_refund_wizard payment.refund.wizard model_payment_refund_wizard account.group_account_invoice 1 1 1 0
4 payment_transaction_user payment.transaction.user model_payment_transaction account.group_account_invoice 1 1 1 0
@@ -1,16 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- Transactions -->
<record id="payment_transaction_billing_rule" model="ir.rule">
<field name="name">Access every transaction</field>
<field name="model_id" ref="payment.model_payment_transaction"/>
<!-- Reset the domain defined by payment.transaction_user_rule -->
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('account.group_account_invoice'))]"/>
</record>
<!-- Tokens -->
<record id="payment_token_billing_rule" model="ir.rule">
@@ -12,19 +12,19 @@
</td>
</xpath>
<xpath expr="//t[@name='invoice_status_posted']/span[1]" position="before">
<t t-set="last_tx" t-value="invoice.get_portal_last_transaction()"/>
<t t-set="tx_sudo" t-value="invoice.get_portal_last_transaction()"/>
</xpath>
<xpath expr="//span[@name='invoice_status_waiting_for_payment']" position="before">
<span t-elif="last_tx.state == 'authorized'"
<span t-elif="tx_sudo.state == 'authorized'"
class="badge rounded-pill text-bg-primary">
<i class="fa fa-fw fa-check"/>
<span class="d-none d-md-inline"> Authorized</span>
</span>
<span t-elif="last_tx.state == 'pending' and last_tx.provider_code not in ('none', 'custom')"
<span t-elif="tx_sudo.state == 'pending' and tx_sudo.provider_code not in ('none', 'custom')"
class="badge rounded-pill text-bg-warning">
<span class="d-none d-md-inline"> Pending</span>
</span>
<span t-elif="last_tx.state == 'done'" class="badge rounded-pill text-bg-success">
<span t-elif="tx_sudo.state == 'done'" class="badge rounded-pill text-bg-success">
<i class="fa fa-fw fa-check"></i>
<span class="d-none d-md-inline"> Paid</span>
</span>
@@ -100,9 +100,9 @@
</t>
<t t-elif="error == 'pay_invoice_tx_fail'">
There was an error processing your payment: transaction failed.<br />
<t t-set="tx_id" t-value="invoice.get_portal_last_transaction()"/>
<t t-if="tx_id and tx_id.state_message">
<t t-out="tx_id.state_message"/>
<t t-set="tx_sudo" t-value="invoice.get_portal_last_transaction()"/>
<t t-if="tx_sudo and tx_sudo.state_message">
<t t-out="tx_sudo.state_message"/>
</t>
</t>
<t t-elif="error == 'pay_invoice_tx_token'">
@@ -115,9 +115,9 @@
inherit_id="account.portal_invoice_success">
<xpath expr="//a[hasclass('close')]" position="after">
<t t-if="success == 'pay_invoice'">
<t t-set="payment_tx_id" t-value="invoice.get_portal_last_transaction()"/>
<span t-if='payment_tx_id.provider_id.sudo().done_msg' t-out="payment_tx_id.provider_id.sudo().done_msg"/>
<div t-if="payment_tx_id.provider_id.sudo().pending_msg and payment_tx_id.provider_code == 'custom' and invoice.ref">
<t t-set="tx_sudo" t-value="invoice.get_portal_last_transaction()"/>
<span t-if="tx_sudo.provider_id.sudo().done_msg" t-out="tx_sudo.provider_id.sudo().done_msg"/>
<div t-if="tx_sudo.provider_id.sudo().pending_msg and tx_sudo.provider_code == 'custom' and invoice.ref">
<b>Communication: </b><span t-out='invoice.ref'/>
</div>
</t>
@@ -7,6 +7,4 @@ payment_method_all,payment.method.all,model_payment_method,,1,0,0,0
payment_method_system,payment.method.system,model_payment_method,base.group_system,1,1,1,1
payment_token_all,payment.token.all,model_payment_token,,1,0,0,0
payment_token_system,payment.token.system,model_payment_token,base.group_system,1,1,1,1
payment_transaction_all,payment.transaction.all,model_payment_transaction,,1,0,0,0
payment_transaction_system,payment.transaction.system,model_payment_transaction,base.group_system,1,1,1,1
payment_transaction_user,payment.transaction.user,model_payment_transaction,base.group_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
7 payment_method_system payment.method.system model_payment_method base.group_system 1 1 1 1
8 payment_token_all payment.token.all model_payment_token 1 0 0 0
9 payment_token_system payment.token.system model_payment_token base.group_system 1 1 1 1
payment_transaction_all payment.transaction.all model_payment_transaction 1 0 0 0
10 payment_transaction_system payment.transaction.system model_payment_transaction base.group_system 1 1 1 1
payment_transaction_user payment.transaction.user model_payment_transaction base.group_user 1 1 1 0
@@ -11,13 +11,6 @@
<!-- Transactions -->
<record id="payment_transaction_user_rule" model="ir.rule">
<field name="name">Access own transactions only</field>
<field name="model_id" ref="payment.model_payment_transaction"/>
<field name="domain_force">['|', ('partner_id', '=', False), ('partner_id', '=', user.partner_id.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user')), (4, ref('base.group_portal'))]"/>
</record>
<record id="transaction_company_rule" model="ir.rule">
<field name="name">Access transactions in own companies only</field>
<field name="model_id" ref="payment.model_payment_transaction"/>
+7 -7
View File
@@ -56,13 +56,13 @@
<p style="margin: 0px; padding: 0px; font-size: 12px;">
Hello,
<br/><br/>
<t t-set="transaction" t-value="object.get_portal_last_transaction()"/>
<t t-set="tx_sudo" t-value="object.get_portal_last_transaction()"/>
Your order <span style="font-weight:bold;" t-out="object.name or ''">S00049</span> amounting in <span style="font-weight:bold;" t-out="format_amount(object.amount_total, object.currency_id) or ''">$ 10.00</span>
<t t-if="object.state == 'sale' or (transaction and transaction.state in ('done', 'authorized'))">
<t t-if="object.state == 'sale' or (tx_sudo and tx_sudo.state in ('done', 'authorized'))">
has been confirmed.<br/>
Thank you for your trust!
</t>
<t t-elif="transaction and transaction.state == 'pending'">
<t t-elif="tx_sudo and tx_sudo.state == 'pending'">
is pending. It will be confirmed when the payment is received.
<t t-if="object.reference">
Your payment reference is <span style="font-weight:bold;" t-out="object.reference or ''"></span>.
@@ -187,13 +187,13 @@
<tr>
<td>
<span style="font-weight:bold;">Payment Method:</span>
<t t-if="transaction.token_id">
<t t-out="transaction.token_id.display_name or ''"></t>
<t t-if="tx_sudo.token_id">
<t t-out="tx_sudo.token_id.display_name or ''"></t>
</t>
<t t-else="">
<t t-out="transaction.provider_id.sudo().name or ''"></t>
<t t-out="tx_sudo.provider_id.sudo().name or ''"></t>
</t>
(<t t-out="format_amount(transaction.amount, object.currency_id) or ''">$ 10.00</t>)
(<t t-out="format_amount(tx_sudo.amount, object.currency_id) or ''">$ 10.00</t>)
</td>
</tr>
</table>
+3 -2
View File
@@ -244,7 +244,8 @@ class SaleOrder(models.Model):
comodel_name='payment.transaction',
string="Authorized Transactions",
compute='_compute_authorized_transaction_ids',
copy=False)
copy=False,
compute_sudo=True)
# UTMs - enforcing the fact that we want to 'set null' when relation is unlinked
campaign_id = fields.Many2one(ondelete='set null')
@@ -1405,7 +1406,7 @@ class SaleOrder(models.Model):
def get_portal_last_transaction(self):
self.ensure_one()
return self.transaction_ids._get_last()
return self.transaction_ids.sudo()._get_last()
def _get_order_lines_to_report(self):
down_payment_lines = self.order_line.filtered(lambda line:
+1 -1
View File
@@ -437,7 +437,7 @@
<ul class="list-group mb-4">
<t t-foreach="invoices" t-as="i">
<t t-set="report_url" t-value="i.get_portal_url(report_type='pdf', download=True)"/>
<t t-set="authorized_tx_ids" t-value="i.transaction_ids.filtered(lambda tx: tx.state == 'authorized')"/>
<t t-set="authorized_tx_ids" t-value="i.authorized_transaction_ids"/>
<div class="d-flex flex-wrap align-items-center justify-content-between">
<div>
<a t-att-href="report_url">
+1 -1
View File
@@ -18,7 +18,7 @@ class WebsiteSaleDelivery(WebsiteSale):
order.access_point_address = {}
carrier_id = int(post['carrier_id'])
if order and carrier_id != order.carrier_id.id:
if any(tx.state not in ('cancel', 'error', 'draft') for tx in order.transaction_ids):
if any(tx.sudo().state not in ('cancel', 'error', 'draft') for tx in order.transaction_ids):
raise UserError(_('It seems that there is already a transaction for your order, you can not change the delivery method anymore'))
order._check_carrier_quotation(force_carrier_id=carrier_id)
return self._update_website_sale_delivery_return(order, **post)
+5 -5
View File
@@ -1606,21 +1606,21 @@ class WebsiteSale(http.Controller):
order = request.env['sale.order'].sudo().browse(sale_order_id)
assert order.id == request.session.get('sale_last_order_id')
tx = order.get_portal_last_transaction() if order else order.env['payment.transaction']
tx_sudo = order.get_portal_last_transaction() if order else order.env['payment.transaction']
if not order or (order.amount_total and not tx):
if not order or (order.amount_total and not tx_sudo):
return request.redirect('/shop')
if order and not order.amount_total and not tx:
if order and not order.amount_total and not tx_sudo:
order.with_context(send_email=True).action_confirm()
return request.redirect(order.get_portal_url())
# clean context and session, then redirect to the confirmation page
request.website.sale_reset()
if tx and tx.state == 'draft':
if tx_sudo and tx_sudo.state == 'draft':
return request.redirect('/shop')
PaymentPostProcessing.remove_transactions(tx)
PaymentPostProcessing.remove_transactions(tx_sudo)
return request.redirect('/shop/confirmation')
@http.route(['/shop/confirmation'], type='http', auth="public", website=True, sitemap=False)
+1 -1
View File
@@ -538,7 +538,7 @@ class SaleOrder(models.Model):
return self.filtered(
lambda abandoned_sale_order:
abandoned_sale_order.partner_id.email
and not any(transaction.state == 'error' for transaction in abandoned_sale_order.transaction_ids)
and not any(transaction.sudo().state == 'error' for transaction in abandoned_sale_order.transaction_ids)
and any(not float_is_zero(line.price_unit, precision_rounding=line.currency_id.rounding) for line in abandoned_sale_order.order_line)
and not has_later_sale_order.get(abandoned_sale_order.partner_id, False)
)
+25 -25
View File
@@ -2155,8 +2155,8 @@
<div class="row">
<div class="col-12 col-xl">
<div class="oe_cart">
<t t-set="payment_tx_id" t-value="order.get_portal_last_transaction()"/>
<t t-if="payment_tx_id.state == 'done'">
<t t-set="tx_sudo" t-value="order.get_portal_last_transaction()"/>
<t t-if="tx_sudo.state == 'done'">
<div class="thanks_msg">
<h2>Thank you for your order.
<a role="button" class="btn btn-primary d-none d-md-inline-block" href="/shop/print" target="_blank" aria-label="Print" title="Print"><i class="fa fa-print"></i> Print</a>
@@ -2173,18 +2173,18 @@
<h3 class="text-start mt-3">
<strong>Payment Information:</strong>
</h3>
<t t-set="payment_tx_id" t-value="order.get_portal_last_transaction()"/>
<t t-set="tx_sudo" t-value="order.get_portal_last_transaction()"/>
<table class="table">
<tbody>
<tr>
<td colspan="2">
<t t-esc="payment_tx_id.provider_id.display_as or payment_tx_id.provider_id.name" />
<t t-esc="tx_sudo.provider_id.display_as or tx_sudo.provider_id.name" />
</td>
<td class="text-end" width="100">
<strong>Total:</strong>
</td>
<td class="text-end" width="100">
<strong t-field="payment_tx_id.amount" t-options="{'widget': 'monetary', 'display_currency': order.currency_id}" />
<strong t-field="tx_sudo.amount" t-options="{'widget': 'monetary', 'display_currency': order.currency_id}" />
</td>
</tr>
</tbody>
@@ -2265,45 +2265,45 @@
<template id="payment_confirmation_status">
<div class="oe_website_sale_tx_status mt-3" t-att-data-order-id="order.id" t-att-data-order-tracking-info="json.dumps(order_tracking_info)">
<t t-set="payment_tx_id" t-value="order.get_portal_last_transaction()"/>
<t t-set="tx_sudo" t-value="order.get_portal_last_transaction()"/>
<div t-attf-class="card #{
(payment_tx_id.state == 'pending' and 'bg-info') or
(payment_tx_id.state == 'done' and order.amount_total == payment_tx_id.amount and 'alert-success') or
(payment_tx_id.state == 'done' and order.amount_total != payment_tx_id.amount and 'bg-warning') or
(payment_tx_id.state == 'authorized' and 'alert-success') or
(tx_sudo.state == 'pending' and 'bg-info') or
(tx_sudo.state == 'done' and order.amount_total == tx_sudo.amount and 'alert-success') or
(tx_sudo.state == 'done' and order.amount_total != tx_sudo.amount and 'bg-warning') or
(tx_sudo.state == 'authorized' and 'alert-success') or
'bg-danger'}">
<div class="card-header">
<a role="button" groups="base.group_system" class="btn btn-sm btn-link text-white float-end" target="_blank" aria-label="Edit" title="Edit"
t-att-href="'/web#model=%s&amp;id=%s&amp;action=%s&amp;view_type=form' % ('payment.provider', payment_tx_id.provider_id.id, 'payment.action_payment_provider')">
t-att-href="'/web#model=%s&amp;id=%s&amp;action=%s&amp;view_type=form' % ('payment.provider', tx_sudo.provider_id.id, 'payment.action_payment_provider')">
<i class="fa fa-pencil"></i>
</a>
<t t-if="payment_tx_id.state == 'pending'">
<t t-out="payment_tx_id.provider_id.sudo().pending_msg"/>
<t t-if="tx_sudo.state == 'pending'">
<t t-out="tx_sudo.provider_id.sudo().pending_msg"/>
</t>
<t t-if="payment_tx_id.state == 'done'">
<span t-if='payment_tx_id.provider_id.sudo().done_msg' t-out="payment_tx_id.provider_id.sudo().done_msg"/>
<t t-if="tx_sudo.state == 'done'">
<span t-if='tx_sudo.provider_id.sudo().done_msg' t-out="tx_sudo.provider_id.sudo().done_msg"/>
</t>
<t t-if="payment_tx_id.state == 'done' and order.amount_total != payment_tx_id.amount">
<t t-if="tx_sudo.state == 'done' and order.amount_total != tx_sudo.amount">
<span>Unfortunately your order can not be confirmed as the amount of your payment does not match the amount of your cart.
Please contact the responsible of the shop for more information.</span>
</t>
<t t-if="payment_tx_id.state == 'cancel'">
<t t-out="payment_tx_id.provider_id.sudo().cancel_msg"/>
<t t-if="tx_sudo.state == 'cancel'">
<t t-out="tx_sudo.provider_id.sudo().cancel_msg"/>
</t>
<t t-if="payment_tx_id.state == 'authorized'">
<t t-if="payment_tx_id.provider_id.sudo().auth_msg" t-out="payment_tx_id.provider_id.sudo().auth_msg"/>
<t t-if="tx_sudo.state == 'authorized'">
<t t-if="tx_sudo.provider_id.sudo().auth_msg" t-out="tx_sudo.provider_id.sudo().auth_msg"/>
<span t-else="">Your payment has been authorized.</span>
</t>
<t t-if="payment_tx_id.state == 'error'">
<span t-esc="payment_tx_id.state_message"/>
<t t-if="tx_sudo.state == 'error'">
<span t-esc="tx_sudo.state_message"/>
</t>
</div>
<t t-if="payment_tx_id.provider_code == 'custom'">
<t t-if="tx_sudo.provider_code == 'custom'">
<div t-if="order.reference" class="card-body">
<b>Communication: </b><span t-esc='order.reference'/>
</div>
<div t-if="payment_tx_id.provider_id.sudo().qr_code">
<t t-set="qr_code" t-value="payment_tx_id.company_id.partner_id.bank_ids[:1].build_qr_code_base64(order.amount_total,payment_tx_id.reference, None, payment_tx_id.currency_id, payment_tx_id.partner_id)"/>
<div t-if="tx_sudo.provider_id.sudo().qr_code">
<t t-set="qr_code" t-value="tx_sudo.company_id.partner_id.bank_ids[:1].build_qr_code_base64(order.amount_total,tx_sudo.reference, None, tx_sudo.currency_id, tx_sudo.partner_id)"/>
<div class="card-body" t-if="qr_code">
<h3>Or scan me with your banking app.</h3>
<img class="border border-dark rounded" t-att-src="qr_code"/>
@@ -14,7 +14,7 @@
<template id="payment_confirmation_status" inherit_id="website_sale.payment_confirmation_status">
<xpath expr="(//div[hasclass('card-body')])[1]" position="replace">
<t t-if="payment_tx_id.provider_id.custom_mode == 'onsite'">
<t t-if="tx_sudo.provider_id.custom_mode == 'onsite'">
<div class="card-body">
<div class="o_header_carrier_message">
<b t-out="order.carrier_id.name"/><span class="text-muted"> (On site picking)</span>