Commit 407b7727 authored by Jérome Perrin's avatar Jérome Perrin

dms: support password protected PDFs in viewer

This is intended to store sensitive documents that users will only
be able to view from ERP5, but not to easily print or download.

To use this, some customization is needed. First, PDF needs to be
saved with a password. Then, PDF_getContentPassword type based method
needs to be customized to return the password.

When using this, we use different password for each document, by
deriving a password from a master key using document properties (such
as document reference for example)
parent b266ef47
# type: () -> bytes
"""Returns the password for this PDF file, if it is known by the system.
This is a customization entry point, used to display the PDF content to
logged in users, if they have the permission to view the PDF.
Note that this is not used for full-text extraction, password protected
PDFs are not indexed, in order not to leak the content.
"""
assert REQUEST is None and not container.REQUEST.args
return None
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>REQUEST=None</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>PDF_getContentPassword</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -12,6 +12,7 @@
<list>
<string>data_url</string>
<string>default</string>
<string>renderjs_extra</string>
</list>
</value>
</item>
......@@ -67,6 +68,12 @@
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>renderjs_extra</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
......@@ -90,6 +97,12 @@
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>renderjs_extra</string> </key>
<value>
<list/>
</value>
</item>
</dictionary>
</value>
</item>
......@@ -109,4 +122,17 @@
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="TALESMethod" module="Products.Formulator.TALESField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>python: field.getTemplateField().get_value("renderjs_extra") + [(\'password\', context.getTypeBasedMethod(\'getContentPassword\')())]</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
/*jslint indent: 2, nomen: true */
/*global window, rJS, RSVP, PDFJS, webViewerLoad, Uint8Array,
ArrayBuffer, PDFViewerApplication, FileReader */
(function (window, rJS, RSVP, PDFJS, webViewerLoad) {
/*global window, rJS, RSVP, PDFJS, configure, webViewerInitialized,
PDFViewerApplication, FileReader, PasswordPrompt */
(function (window, rJS, RSVP, PDFJS, configure, webViewerInitialized, PDFViewerApplication, PasswordPrompt) {
"use strict";
rJS(window)
......@@ -17,6 +17,17 @@
gadget.props.key = options.key;
configure(PDFJS);
PDFJS.locale = options.language;
if (options.password) {
PasswordPrompt._original_open = PasswordPrompt.open;
var retries = 0;
PasswordPrompt.open = function () {
if (retries) {
return this._original_open();
}
retries++;
return this.updatePassword(options.password);
};
}
return PDFViewerApplication.initialize().then(function() {
webViewerInitialized(options.value);
// hide some buttons that do not make sense for us
......@@ -52,4 +63,4 @@
return form_data;
});
});
}(window, rJS, RSVP, PDFJS, webViewerLoad, PDFViewerApplication));
}(window, rJS, RSVP, PDFJS, configure, webViewerInitialized, PDFViewerApplication, PasswordPrompt));
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PDF" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Access_contents_information_Permission</string> </key>
<value>
<tuple>
<string>Anonymous</string>
<string>Assignee</string>
<string>Assignor</string>
<string>Associate</string>
<string>Auditor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Add_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Change_local_roles_Permission</string> </key>
<value>
<tuple>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_EtagSupport__etag</string> </key>
<value> <string>ts54848644.25</string> </value>
</item>
<item>
<key> <string>_Modify_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_View_Permission</string> </key>
<value>
<tuple>
<string>Anonymous</string>
<string>Assignee</string>
<string>Assignor</string>
<string>Associate</string>
<string>Auditor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_count</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>_mt_index</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>_tree</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
<item>
<key> <string>content_md5</string> </key>
<value> <string>e0751dd9b313aa3cc08d7cbfebf0d100</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>application/pdf</string> </value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>test-ERP5.Logo.Encrypted.PDF</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string>A sample PDF with text and an image, encrypted with password "secret"</string> </value>
</item>
<item>
<key> <string>filename</string> </key>
<value> <string>test-ERP5.Logo.encrypted.PDF.pdf</string> </value>
</item>
<item>
<key> <string>height</string> </key>
<value> <int>-1</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>test_ERP5_Logo_Encrypted_PDF</string> </value>
</item>
<item>
<key> <string>language</string> </key>
<value> <string>en</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>PDF</string> </value>
</item>
<item>
<key> <string>short_title</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>size</string> </key>
<value> <int>7745</int> </value>
</item>
<item>
<key> <string>title</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>001</string> </value>
</item>
<item>
<key> <string>width</string> </key>
<value> <int>-1</int> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAU=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Length" module="BTrees.Length"/>
</pickle>
<pickle> <int>0</int> </pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
<record id="5" aka="AAAAAAAAAAU=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>document_publication_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAY=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="6" aka="AAAAAAAAAAY=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_log</string> </key>
<value>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>publish</string> </value>
</item>
<item>
<key> <string>actor</string> </key>
<value> <string>ERP5TypeTestCase</string> </value>
</item>
<item>
<key> <string>comment</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>error_message</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>time</string> </key>
<value>
<object>
<klass>
<global name="DateTime" module="DateTime.DateTime"/>
</klass>
<tuple>
<none/>
</tuple>
<state>
<tuple>
<float>1655084957.23</float>
<string>GMT+2</string>
</tuple>
</state>
</object>
</value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>published</string> </value>
</item>
</dictionary>
</list>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>expand</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>testEncryptedPDFPreview</string> </value>
</item>
<item>
<key> <string>output_encoding</string> </key>
<value> <string>utf-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <unicode>Encrypted PDF Preview</unicode> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title tal:content="template/title_and_id"></title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><td rowspan="1" colspan="3" tal:content="template/title_and_id"></td></tr>
</thead><tbody>
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/init" />
<tr>
<td>openAndWait</td>
<td>${base_url}/document_module/test_ERP5_Logo_Encrypted_PDF</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>link=Preview</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>//iframe[contains(@src, "pdfjs.gadget.html")]</td>
<td></td>
</tr>
<tr>
<td>selectFrame</td>
<td>//iframe[contains(@src, "pdfjs.gadget.html")]</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>//body//div[@class='textLayer']/div[1]</td>
<td>This is a sample PDF with some text and an image</td>
</tr>
<tr>
<td>selectFrame</td>
<td>relative=top</td>
<td></td>
</tr>
<!--
This PDF is automatically decrypted because of the ID
(see portal_skins/erp5_dms_ui_test/PDF_getContentPassword)
Make a clone, that will not be automatically decrypted (because Id will be different)
It is still possible for user to enter a password and view.
-->
<tr>
<td>clickAndWait</td>
<td>Base_createCloneDocument:method</td>
<td></td>
</tr>
<tr>
<td>assertPortalStatusMessage</td>
<td>Created Clone PDF.</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>//iframe[contains(@src, "pdfjs.gadget.html")]</td>
<td></td>
</tr>
<tr>
<td>selectFrame</td>
<td>//iframe[contains(@src, "pdfjs.gadget.html")]</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>//div[@id="passwordOverlay" and not(contains(@class, "hidden"))]</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>password</td>
<td>secret</td>
</tr>
<tr>
<td>click</td>
<td>passwordSubmit</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>//body//div[@class='textLayer']/div[1]</td>
<td>This is a sample PDF with some text and an image</td>
</tr>
<tr>
<td>selectFrame</td>
<td>relative=top</td>
<td></td>
</tr>
</tbody></table>
</body>
</html>
\ No newline at end of file
......@@ -6,12 +6,33 @@
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_local_properties</string> </key>
<value>
<tuple>
<dictionary>
<item>
<key> <string>id</string> </key>
<value> <string>business_template_skin_layer_priority</string> </value>
</item>
<item>
<key> <string>type</string> </key>
<value> <string>float</string> </value>
</item>
</dictionary>
</tuple>
</value>
</item>
<item>
<key> <string>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>business_template_skin_layer_priority</string> </key>
<value> <float>40.0</float> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>erp5_dms_ui_test</string> </value>
......
# type: () -> bytes
if context.getId() == 'test_ERP5_Logo_Encrypted_PDF':
return 'secret'
return context.skinSuper('erp5_dms_ui_test', 'PDF_getContentPassword')(REQUEST=REQUEST)
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>REQUEST=None</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>PDF_getContentPassword</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
document_module/test_ERP5_Logo_PDF
\ No newline at end of file
document_module/test_ERP5_Logo_PDF
document_module/test_ERP5_Logo_Encrypted_PDF
\ No newline at end of file
document_module/test_ERP5_Logo_Encrypted_PDF
document_module/test_ERP5_Logo_PDF
portal_categories/classification/**
portal_tests/dms_zuite
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>expand</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>testEncryptedPDFPreview</string> </value>
</item>
<item>
<key> <string>output_encoding</string> </key>
<value> <string>utf-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <unicode>Encrypted PDF Preview</unicode> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title tal:content="template/title_and_id"></title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><td rowspan="1" colspan="3" tal:content="template/title_and_id"></td></tr>
</thead><tbody>
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/init" />
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/wait_for_activities" />
<!-- Access a PDF provided by erp5_dms_ui_test -->
<tr>
<td>open</td>
<td>${base_url}/web_site_module/renderjs_runner/#/document_module/test_ERP5_Logo_Encrypted_PDF?editable=1</td>
<td></td>
</tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_app_loaded" />
<tal:block tal:define="click_configuration python: {'text': 'Views'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/click_on_header_link" />
</tal:block>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_content_loaded" />
<tal:block tal:define="click_configuration python: {'text': 'Preview'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/click_on_page_link" />
</tal:block>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_content_loaded" />
<tr>
<td>waitForElementPresent</td>
<td>//iframe[contains(@src, "pdfjs.gadget.html")]</td>
<td></td>
</tr>
<tr>
<td>selectFrame</td>
<td>//iframe[contains(@src, "pdfjs.gadget.html")]</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>//body//div[@class='textLayer']/div[1]</td>
<td>This is a sample PDF with some text and an image</td>
</tr>
<tr>
<td>selectFrame</td>
<td>relative=top</td>
<td></td>
</tr>
<!--
This PDF is automatically decrypted because of the ID
(see portal_skins/erp5_dms_ui_test/PDF_getContentPassword)
Make a clone, that will not be automatically decrypted (because Id will be different)
It is still possible for user to enter a password and view.
-->
<tal:block tal:define="click_configuration python: {'text': 'Actions'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/click_on_header_link" />
</tal:block>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_content_loaded" />
<tal:block tal:define="click_configuration python: {'text': 'Clone Document'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/click_on_page_link" />
</tal:block>
<tal:block tal:define="notification_configuration python: {'class': 'success',
'text': 'Created Clone PDF.'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_notification" />
</tal:block>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_content_loaded" />
<tal:block tal:define="click_configuration python: {'text': 'Views'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/click_on_header_link" />
</tal:block>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_content_loaded" />
<tal:block tal:define="click_configuration python: {'text': 'Preview'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/click_on_page_link" />
</tal:block>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_content_loaded" />
<tr>
<td>waitForElementPresent</td>
<td>//iframe[contains(@src, "pdfjs.gadget.html")]</td>
<td></td>
</tr>
<tr>
<td>selectFrame</td>
<td>//iframe[contains(@src, "pdfjs.gadget.html")]</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>//div[@id="passwordOverlay" and not(contains(@class, "hidden"))]</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>password</td>
<td>secret</td>
</tr>
<tr>
<td>click</td>
<td>passwordSubmit</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>//body//div[@class='textLayer']/div[1]</td>
<td>This is a sample PDF with some text and an image</td>
</tr>
<tr>
<td>selectFrame</td>
<td>relative=top</td>
<td></td>
</tr>
</tbody></table>
</body>
</html>
\ No newline at end of file
......@@ -14,6 +14,7 @@
* can modify the content, false for a "read only" editor
* @property {string} language the user language, if the editor supports
* localisation it will be displayed in this language
* @property {string} password a password to decrypt the content
* @property {boolean} run a hack for jsmd editor
* @property {string} key Key for ERP5 form
*/
......@@ -88,6 +89,7 @@
//this is temporary until the viewer becomes editable
run: options.run || false,
key: options.key,
password: options.password,
// Force calling subfield render
// as user may have modified the input value
render_timestamp: new Date().getTime()
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment