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

Pending job sync #8104

Open
wants to merge 5 commits into
base: es/TI-1494-non-signers-sign-request
Choose a base branch
from
Open
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 changes/TI-1494-2.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Provides an endpoint to sync pending signing jobs with the external sign service. [elioschmutz]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Provides an endpoint to sync pending signing jobs with the external sign service. [elioschmutz]
Provide an endpoint to sync pending signing jobs with the external sign service. [elioschmutz]

2 changes: 1 addition & 1 deletion docs/public/dev-manual/api/api_changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Breaking Changes

Other Changes
^^^^^^^^^^^^^

- ``@update-pending-signing-job``: Add endpoint to update pending signing job

2024.18.0 (2024.12.13)
----------------------
Expand Down
29 changes: 29 additions & 0 deletions docs/public/dev-manual/api/documents_sign.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,35 @@ Der Request löst den Signaturprozess für das Dokument aus.

Im Rahmen des Signaturprozesses wird ein Access-Token generiert, der dem externen Signaturservice übergeben wird. Dieses Token muss im späteren Request zur Rückführung der signierten PDF-Datei wiederverwendet werden.

Aktualisieren eines ausstehenden Signierungsauftrags
----------------------------------------------------
Der Endpoint ``@update-pending-signing-job`` dient dazu, die Listen der Signierenden ("signers") und Bearbeitenden ("editors") eines ausstehenden Signierungsauftrags zu aktualisieren. Dies ermöglicht es einem externen Signierungsdienst, Änderungen in GEVER vorzunehmen und die betroffenen Personen entsprechend anzupassen.

**Beispiel-Request**:

.. code-block:: http

PATCH /@update-pending-signing-job HTTP/1.1
Content-Type: application/json
Authorization: Bearer <access_token>

{
"access_token": "12345",
"signature_data": {
"signers": ["[email protected]"],
"editors": ["[email protected]"]
}
}

- ``access_token``: Das beim Start des Signaturprozesses generierte Token.
- ``signature_data`` (erforderlich): Ein Objekt, das die zu aktualisierenden Felder enthält.
- ``signers`` (optional): Eine Liste von E-Mail-Adressen der neuen Signierenden. Wenn nicht angegeben, bleibt die Liste der Signierenden unverändert.
- ``editors`` (optional): Eine Liste von E-Mail-Adressen der neuen Bearbeitenden. Wenn nicht angegeben, bleibt die Liste der Bearbeitenden unverändert.

**Hinweise**

- Die Aktualisierung gilt nur für ausstehende Signierungsaufträge und hat keine Auswirkungen auf bereits abgeschlossene Vorgänge.

Hochladen der signierten PDF-Datei
----------------------------------

Expand Down
8 changes: 8 additions & 0 deletions opengever/api/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -1857,4 +1857,12 @@
permission="zope.Public"
/>

<plone:service
method="PATCH"
name="@update-pending-signing-job"
for="opengever.document.document.IDocumentSchema"
factory=".sign.UpdatePendingSigningJob"
permission="zope.Public"
/>

</configure>
42 changes: 42 additions & 0 deletions opengever/api/sign.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,45 @@ def extract_payload(self):

self.access_token = access_token
self.signed_pdf_data = signed_pdf_data


class UpdatePendingSigningJob(Service):
"""Endpoint for updating metadata for a pending signing job"""

def __call__(self):
if api.content.get_state(obj=self.context) != Document.signing_state:
raise Forbidden()

self.extract_payload()
self.signer = Signer(self.context)

return super(UpdatePendingSigningJob, self).__call__()

def reply(self):
with self.signer.adopt_issuer():
self.signer.update_pending_signing_job(**self.data)

self.request.response.setStatus(200)
return self.signer.serialize_pending_signing_job()

def check_permission(self):
try:
self.signer.validate_token(self.access_token)
except InvalidToken:
raise Unauthorized()

def extract_payload(self):
data = json_body(self.request)
access_token = data.get('access_token')
if not access_token:
raise BadRequest("Property 'access_token' is required")

data = data.get('signature_data')
if not data:
raise BadRequest("Property 'signature_data' is required")

self.access_token = access_token
self.data = {
'signers': data.get('signers'),
'editors': data.get('editors'),
}
116 changes: 116 additions & 0 deletions opengever/api/tests/test_sign.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,119 @@ def test_return_sign_redirect_url_when_signing_a_document_from_final_state(self,

self.assertEqual('http://external.example.org/signing-requests/123',
browser.json.get('redirect_url'))


class TestUpdatePendingSigningJobPost(SolrIntegrationTestCase):

features = ['sign']

@browsing
def test_raises_forbidden_if_context_is_not_in_signing_state(self, browser):
with self.login(self.regular_user):
url = self.document.absolute_url() + '/@update-pending-signing-job'

browser.exception_bubbling = True
with self.assertRaises(Forbidden):
browser.open(url, method='PATCH', headers=self.api_headers)

@browsing
@requests_mock.Mocker()
def test_access_token_is_required_in_payload(self, browser, mocker):
mocker.post(re.compile('/signing-jobs'), json=DEFAULT_MOCK_RESPONSE)

with self.login(self.regular_user, browser):
browser.open(self.document.absolute_url() + '/@workflow/' + Document.draft_signing_transition,
method='POST',
headers=self.api_headers)

url = self.document.absolute_url() + '/@update-pending-signing-job'

with browser.expect_http_error(400):
browser.open(url,
method='PATCH',
headers=self.api_headers,
data=json.dumps({}))

self.assertEqual(
{'message': "Property 'access_token' is required",
'type': 'BadRequest'},
browser.json)

@browsing
@requests_mock.Mocker()
def test_requires_a_valid_access_token(self, browser, mocker):
mocker.post(re.compile('/signing-jobs'), json=DEFAULT_MOCK_RESPONSE)

with self.login(self.regular_user, browser):
browser.open(self.document.absolute_url() + '/@workflow/' + Document.draft_signing_transition,
method='POST',
headers=self.api_headers)

url = self.document.absolute_url() + '/@update-pending-signing-job'

browser.exception_bubbling = True
with self.assertRaises(Unauthorized):
browser.open(url,
method='PATCH',
headers=self.api_headers,
data=json.dumps({
'access_token': urlsafe_b64encode('<invalid-token>'),
'signature_data': {
'editors': ['[email protected]'],
'signers': ['[email protected]'],
}
}))

@browsing
@requests_mock.Mocker()
def test_data_attribute_is_required_in_payload(self, browser, mocker):
mocker.post(re.compile('/signing-jobs'), json=DEFAULT_MOCK_RESPONSE)

with self.login(self.regular_user, browser):
browser.open(self.document.absolute_url() + '/@workflow/' + Document.draft_signing_transition,
method='POST',
headers=self.api_headers)

url = self.document.absolute_url() + '/@update-pending-signing-job'

with browser.expect_http_error(400):
browser.open(url,
method='PATCH',
headers=self.api_headers,
data=json.dumps({
'access_token': urlsafe_b64encode('<invalid-token>'),
}))

self.assertEqual(
{'message': "Property 'signature_data' is required",
'type': 'BadRequest'},
browser.json)

@browsing
@requests_mock.Mocker()
def test_can_update_signers_and_editors_of_pending_job(self, browser, mocker):
mocker.post(re.compile('/signing-jobs'), json=DEFAULT_MOCK_RESPONSE)

with self.login(self.regular_user, browser=browser):
browser.open(self.document.absolute_url() + '/@workflow/' + Document.draft_signing_transition,
method='POST',
headers=self.api_headers)

token = urlsafe_b64encode(Signer(self.document).token_manager._get_token())
url = self.document.absolute_url() + '/@update-pending-signing-job'

browser.open(url,
method='PATCH',
headers=self.api_headers,
data=json.dumps({
'access_token': token,
'signature_data': {
'editors': ['[email protected]'],
'signers': ['[email protected]'],
}
}))

self.assertEqual([{'email': '[email protected]', 'userid': ''}],
browser.json.get('editors'))
self.assertEqual([{'email': '[email protected]', 'userid': ''}],
browser.json.get('signers'))
1 change: 1 addition & 0 deletions opengever/base/subscribers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
'POST_application_json_@logout',
'POST_application_json_@login-renew',
'POST_application_json_@upload-signed-pdf',
'PATCH_application_json_@update-pending-signing-job',
'customlogo',
'customlogo_right',
'dump-content-stats',
Expand Down
1 change: 1 addition & 0 deletions opengever/sign/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def queue_signing(self, document, token, signers, editors):
'document_url': document.absolute_url(),
'download_url': bumblebee_service.get_download_url(document),
'upload_url': '{}/@upload-signed-pdf'.format(document.absolute_url()),
'update_url': '{}/@update-pending-signing-job'.format(document.absolute_url()),
'document_uid': document.UID(),
'title': document.title_or_id(),
'signers': signers,
Expand Down
5 changes: 5 additions & 0 deletions opengever/sign/pending_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@


class PendingEditors(PersistentList):

@classmethod
def from_emails(cls, emails):
return cls([PendingEditor(email=email) for email in emails])

def serialize(self):
return json_compatible([pending_editor.serialize() for pending_editor in self])

Expand Down
5 changes: 5 additions & 0 deletions opengever/sign/pending_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@


class PendingSigners(PersistentList):

@classmethod
def from_emails(cls, emails):
return cls([PendingSigner(email=email) for email in emails])

def serialize(self):
return json_compatible([pending_signer.serialize() for pending_signer in self])

Expand Down
15 changes: 11 additions & 4 deletions opengever/sign/pending_signing_job.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from datetime import datetime
from opengever.sign.pending_editor import PendingEditor
from opengever.sign.pending_editor import PendingEditors
from opengever.sign.pending_signer import PendingSigner
from opengever.sign.pending_signer import PendingSigners
from opengever.sign.signed_version import SignedVersion
from persistent import Persistent
Expand All @@ -24,8 +22,8 @@ def __init__(self,
self.created = created or datetime.now()
self.userid = userid
self.version = version
self.signers = PendingSigners([PendingSigner(email=signer) for signer in signers])
self.editors = PendingEditors([PendingEditor(email=editor) for editor in editors])
self.signers = PendingSigners.from_emails(signers)
self.editors = PendingEditors.from_emails(editors)
self.job_id = job_id
self.redirect_url = redirect_url
self.invite_url = invite_url
Expand All @@ -47,3 +45,12 @@ def to_signed_version(self):
signatories=self.signers.to_signatories(),
version=self.version + 1
)

def update(self, **data):
signers = data.get('signers')
if isinstance(signers, list):
self.signers = PendingSigners.from_emails(signers)

editors = data.get('editors')
if isinstance(editors, list):
self.editors = PendingEditors.from_emails(editors)
3 changes: 3 additions & 0 deletions opengever/sign/sign.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ def adopt_issuer(self):
raise IssuerNotFound()
return api.env.adopt_user(user=user)

def update_pending_signing_job(self, **data):
self.pending_signing_job.update(**data)

def serialize_pending_signing_job(self):
return self.pending_signing_job.serialize() if self.pending_signing_job else {}

Expand Down
1 change: 1 addition & 0 deletions opengever/sign/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def test_add_a_signing_job(self, mocker):
u'editors': ['[email protected]'],
u'title': u'Vertr\xe4gsentwurf',
u'upload_url': u'http://nohost/plone/ordnungssystem/fuhrung/vertrage-und-vereinbarungen/dossier-1/document-14/@upload-signed-pdf', # noqa
u'update_url': u'http://nohost/plone/ordnungssystem/fuhrung/vertrage-und-vereinbarungen/dossier-1/document-14/@update-pending-signing-job', # noqa
},
mocker.last_request.json())

Expand Down
8 changes: 8 additions & 0 deletions opengever/sign/tests/test_pending_editors.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@ def test_can_be_serialized(self):
self.assertEqual(2, len(container.serialize()))
self.assertItemsEqual([signer2.email, signer1.email],
[item.get('email') for item in container.serialize()])

def test_can_be_created_from_a_list_of_emails(self):
container = PendingEditors.from_emails(['[email protected]', '[email protected]'])

self.assertEqual(2, len(container))
self.assertTrue(isinstance(container[0], PendingEditor))
self.assertItemsEqual(['[email protected]', '[email protected]'],
[item.get('email') for item in container.serialize()])
8 changes: 8 additions & 0 deletions opengever/sign/tests/test_pending_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,11 @@ def test_can_be_converted_to_signatories(self):
container.append(PendingSigner())

self.assertEqual(2, len(container.to_signatories()))

def test_can_be_created_from_a_list_of_emails(self):
container = PendingSigners.from_emails(['[email protected]', '[email protected]'])

self.assertEqual(2, len(container))
self.assertTrue(isinstance(container[0], PendingSigner))
self.assertItemsEqual(['[email protected]', '[email protected]'],
[item.get('email') for item in container.serialize()])
39 changes: 39 additions & 0 deletions opengever/sign/tests/test_pending_signing_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,42 @@ def test_can_be_converted_to_a_signed_version(self):
],
'version': 2
}, data)

def test_can_update_signers_and_editors(self):
self.login(self.regular_user)

pending_signing_job = PendingSigningJob(signers=[], editors=[])

self.assertItemsEqual([], pending_signing_job.serialize().get('signers'))
self.assertItemsEqual([], pending_signing_job.serialize().get('editors'))

pending_signing_job.update(signers=['[email protected]'])

self.assertItemsEqual(
[{u'userid': u'', u'email': u'[email protected]'}],
pending_signing_job.serialize().get('signers'))

self.assertItemsEqual(
[],
pending_signing_job.serialize().get('editors'))

pending_signing_job.update(editors=['[email protected]'])

self.assertItemsEqual(
[{u'userid': u'', u'email': u'[email protected]'}],
pending_signing_job.serialize().get('signers'))

self.assertItemsEqual(
[{u'userid': u'', u'email': u'[email protected]'}],
pending_signing_job.serialize().get('editors'))

pending_signing_job.update(signers=['[email protected]'],
editors=['[email protected]'])

self.assertItemsEqual(
[{u'userid': u'', u'email': u'[email protected]'}],
pending_signing_job.serialize().get('signers'))

self.assertItemsEqual(
[{u'userid': u'', u'email': u'[email protected]'}],
pending_signing_job.serialize().get('editors'))
Loading