Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IMP] sign_oca: Ensure inalterability #15

Merged
merged 2 commits into from
Aug 25, 2024
Merged
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
1 change: 1 addition & 0 deletions sign_oca/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"views/menu.xml",
"data/data.xml",
"wizards/res_config_settings_views.xml",
"data/ir_sequence_data.xml",
"wizards/sign_oca_template_generate.xml",
"wizards/sign_oca_template_generate_multi.xml",
"views/res_partner_views.xml",
Expand Down
8 changes: 3 additions & 5 deletions sign_oca/controllers/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import base64
import io

from odoo import http
from odoo.exceptions import AccessError, MissingError
from odoo.http import request
Expand Down Expand Up @@ -70,8 +67,9 @@ def get_sign_oca_content_access(self, signer_id, access_token):
)
except (AccessError, MissingError):
return request.redirect("/my")
data = io.BytesIO(base64.standard_b64decode(signer_sudo.request_id.data))
return http.send_file(data, filename=signer_sudo.request_id.name)
return http.Stream.from_binary_field(
signer_sudo.request_id, "data"
).get_response(mimetype="application/pdf")

@http.route(
["/sign_oca/info/<int:signer_id>/<string:access_token>"],
Expand Down
12 changes: 12 additions & 0 deletions sign_oca/data/ir_sequence_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo noupdate="1">
<record id="sign_inalterability_sequence" model="ir.sequence">
<field name="name">Securization of Signature</field>
<field name="code">SECUR_SIGN</field>
<field name="implementation">no_gap</field>
<field name="prefix" />
<field name="suffix" />
<field name="padding">0</field>
<field name="company_id" eval="False" />
</record>
</odoo>
97 changes: 94 additions & 3 deletions sign_oca/models/sign_oca_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

import hashlib
import json
from base64 import b64decode, b64encode
from hashlib import sha256
from io import BytesIO

from PyPDF2 import PdfFileReader, PdfFileWriter
Expand All @@ -13,8 +15,9 @@
from reportlab.platypus import Image, Paragraph

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.exceptions import UserError, ValidationError
from odoo.http import request
from odoo.tools import float_repr


class SignOcaRequest(models.Model):
Expand Down Expand Up @@ -109,9 +112,10 @@
)

@api.depends(
"signer_id",
"signer_id.is_allow_signature",
"signer_ids",
"signer_ids.is_allow_signature",
)
@api.depends_context("uid")
def _compute_to_sign(self):
for record in self:
record.to_sign = (
Expand Down Expand Up @@ -351,6 +355,19 @@
model = fields.Char(compute="_compute_model", store=True)
res_id = fields.Integer(compute="_compute_res_id", store=True)
is_allow_signature = fields.Boolean(compute="_compute_is_allow_signature")
secure_sequence_number = fields.Integer(
string="Inalteralbility No Gap Sequence #",
readonly=True,
copy=False,
index=True,
)
inalterable_hash = fields.Char(
string="Inalterability Hash", readonly=True, copy=False
)
sequence_id = fields.Many2one(
"ir.sequence", copy=False, default=lambda r: r._get_sequence()
)
altered_hash = fields.Boolean(compute="_compute_altered_hash")

@api.depends("request_id.record_ref")
def _compute_model(self):
Expand Down Expand Up @@ -460,6 +477,15 @@
self.signature_hash = final_hash
self.request_id._check_signed()
self._set_action_log("sign", access_token=access_token)
if self.sequence_id:
self.flush_recordset()
new_number = self.sequence_id.next_by_id()
self.write(
{
"secure_sequence_number": new_number,
"inalterable_hash": self._get_new_hash(new_number),
}
)
self.request_id.action_send_signed_request()
return {
"type": "ir.actions.act_url",
Expand Down Expand Up @@ -556,6 +582,71 @@
result = [(signer.id, (signer.partner_id.display_name)) for signer in self]
return result

def _get_sequence(self):
return self.env.ref("sign_oca.sign_inalterability_sequence")

@api.depends(
lambda r: ["request_id.data", "inalterable_hash", "secure_sequence_number"]
+ r._get_integrity_hash_fields()
)
def _compute_altered_hash(self):
for record in self:
record.altered_hash = (
record.inalterable_hash
and record.inalterable_hash
!= record._get_new_hash(record.secure_sequence_number)
)

def _get_new_hash(self, secure_seq_number):
prev_sign = self.sudo().search(
[
("sequence_id", "=", self.sequence_id.id),
("secure_sequence_number", "!=", 0),
("secure_sequence_number", "=", int(secure_seq_number) - 1),
]
)
if prev_sign and len(prev_sign) != 1:
raise UserError(

Check warning on line 609 in sign_oca/models/sign_oca_request.py

View check run for this annotation

Codecov / codecov/patch

sign_oca/models/sign_oca_request.py#L609

Added line #L609 was not covered by tests
_(
"An error occurred when computing the inalterability. "
"Impossible to get the unique previous signer information."
)
)
return self._compute_hash(prev_sign.inalterable_hash if prev_sign else "")

def _compute_hash(self, previous_hash):
"""Computes the hash of the browse_record given as self, based on the hash
of the previous record in the company's securisation sequence given as parameter"""
self.ensure_one()
hash_string = sha256((previous_hash + self._string_to_hash()).encode("utf-8"))
return hash_string.hexdigest()

def _string_to_hash(self):
def _getattrstring(obj, field_str):
field_value = obj[field_str]
if obj._fields[field_str].type == "many2one":
field_value = field_value.id
if obj._fields[field_str].type == "monetary":
return float_repr(field_value, obj.currency_id.decimal_places)

Check warning on line 630 in sign_oca/models/sign_oca_request.py

View check run for this annotation

Codecov / codecov/patch

sign_oca/models/sign_oca_request.py#L630

Added line #L630 was not covered by tests
return str(field_value)

values = {"items": {}}
for field in self._get_integrity_hash_fields():
values[field] = _getattrstring(self, field)
for key, signatory_value in self.request_id.signatory_data.items():
if signatory_value["role_id"] == self.role_id.id:
values[key] = signatory_value
return json.dumps(
values,
sort_keys=True,
ensure_ascii=True,
indent=None,
separators=(",", ":"),
)

def _get_integrity_hash_fields(self):
return ["partner_id", "role_id", "signed_on", "signature_hash"]


class SignRequestLog(models.Model):
_name = "sign.oca.request.log"
Expand Down
2 changes: 1 addition & 1 deletion sign_oca/models/sign_oca_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def _prepare_sign_oca_request_vals_from_record(self, record):
class SignOcaTemplateItem(models.Model):

_name = "sign.oca.template.item"
_description = "Sign Oca Template Item" # TODO
_description = "Sign Oca Template Item"

template_id = fields.Many2one(
"sign.oca.template", required=True, ondelete="cascade"
Expand Down
38 changes: 38 additions & 0 deletions sign_oca/tests/test_sign.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,41 @@ def test_auto_sign_template_cancel(self):
signer = self.template.request_ids.signer_id
signer.request_id.cancel()
self.assertEqual(signer.request_id.state, "cancel")

def test_inalterability(self):
self.configure_template()
f = Form(
self.env["sign.oca.template.generate"].with_context(
default_template_id=self.template.id, default_sign_now=True
)
)
f.save().generate()
signer = self.template.request_ids.signer_id
data = {}
for key in signer.get_info()["items"]:
val = signer.get_info()["items"][key].copy()
val["value"] = "My Name"
data[key] = val
signer.action_sign(data)
self.assertFalse(signer.altered_hash)
f = Form(
self.env["sign.oca.template.generate"].with_context(
default_template_id=self.template.id, default_sign_now=True
)
)
f.save().generate()
signer_2 = self.template.request_ids.signer_id.filtered(lambda r: r != signer)
data = {}
for key in signer_2.get_info()["items"]:
val = signer_2.get_info()["items"][key].copy()
val["value"] = "My Name"
data[key] = val
signer_2.action_sign(data)
self.assertFalse(signer_2.altered_hash)
signer.signature_hash = signer.signature_hash + "AA"
self.assertTrue(signer.altered_hash)
signer_2.invalidate_recordset()
self.assertFalse(signer_2.altered_hash)
signer.inalterable_hash = signer._get_new_hash(signer.secure_sequence_number)
signer_2.invalidate_recordset()
self.assertTrue(signer_2.altered_hash)
4 changes: 3 additions & 1 deletion sign_oca/views/sign_oca_request.xml
Original file line number Diff line number Diff line change
Expand Up @@ -325,11 +325,12 @@
<field name="name">sign.oca.request.signer.tree (in sign_oca)</field>
<field name="model">sign.oca.request.signer</field>
<field name="arch" type="xml">
<tree edit="0" delete="0" create="0">
<tree edit="0" delete="0" create="0" decoration-danger="altered_hash">
<field name="role_id" />
<field name="partner_id" />
<field name="request_id" />
<field name="signed_on" />
<field name="altered_hash" invisible="1" />
</tree>
</field>
</record>
Expand All @@ -353,6 +354,7 @@
<field name="partner_id" readonly="1" />
<field name="request_id" readonly="1" />
<field name="signed_on" />
<field name="inalterable_hash" />
</group>
</sheet>
</form>
Expand Down
Loading