diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 88d5d0648..0e3280800 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -51,7 +51,6 @@ jobs: - name: Python Setup working-directory: cla-backend run: | - pip install --platform manylinux_2_12_x86_64 --only-binary=:all: --target=/opt/hostedtoolcache/Python/3.7.17/x64/lib/python3.7/site-packages cryptography pip install -r requirements.txt - name: Python Lint diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 6c69d9545..85013d15b 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -88,7 +88,6 @@ jobs: - name: Python Setup working-directory: cla-backend run: | - pip install --platform manylinux_2_12_x86_64 --only-binary=:all: --target=/opt/hostedtoolcache/Python/3.7.17/x64/lib/python3.7/site-packages cryptography pip install -r requirements.txt - name: Python Lint diff --git a/cla-backend-go/serverless.yml b/cla-backend-go/serverless.yml index 38511351a..8f96b02f9 100644 --- a/cla-backend-go/serverless.yml +++ b/cla-backend-go/serverless.yml @@ -233,8 +233,9 @@ provider: SF_USERNAME: ${file(./env.json):sf-username, ssm:/cla-sf-username-${opt:stage}} SF_PASSWORD: ${file(./env.json):sf-password, ssm:/cla-sf-password-${opt:stage}} DOCRAPTOR_API_KEY: ${file(./env.json):doc-raptor-api-key, ssm:/cla-doc-raptor-api-key-${opt:stage}} - DOCUSIGN_AUTH_SERVER: ${file(./env.json):docusign-auth-server, ssm:/cla-docusign-auth-server-${sls:stage}} - DOCUSIGN_USER_ID: ${file(./env.json):docusign-user-id, ssm:/cla-docusign-user-id-${sls:stage}} + DOCUSIGN_ROOT_URL: ${file(./env.json):docusign-root-url, ssm:/cla-docusign-root-url-${opt:stage}} + DOCUSIGN_USERNAME: ${file(./env.json):docusign-username, ssm:/cla-docusign-username-${opt:stage}} + DOCUSIGN_PASSWORD: ${file(./env.json):docusign-password, ssm:/cla-docusign-password-${opt:stage}} DOCUSIGN_INTEGRATOR_KEY: ${file(./env.json):docusign-integrator-key, ssm:/cla-docusign-integrator-key-${opt:stage}} CLA_API_BASE: ${file(./env.json):cla-api-base, ssm:/cla-api-base-${opt:stage}} CLA_CONTRIBUTOR_BASE: ${file(./env.json):cla-contributor-base, ssm:/cla-contributor-base-${opt:stage}} diff --git a/cla-backend/Makefile b/cla-backend/Makefile index ff24a7433..a37ff56d3 100644 --- a/cla-backend/Makefile +++ b/cla-backend/Makefile @@ -12,7 +12,6 @@ setup: .PHONY: setup_circle setup_circle: npm install; \ - sudo pip install --platform manylinux_2_12_x86_64 --only-binary=:all: --target=/opt/hostedtoolcache/Python/3.7.17/x64/lib/python3.7/site-packages cryptography; \ sudo pip install -r requirements.txt; \ # username: LFID username diff --git a/cla-backend/cla/models/docusign_models.py b/cla-backend/cla/models/docusign_models.py index 5488d1502..756335585 100644 --- a/cla-backend/cla/models/docusign_models.py +++ b/cla-backend/cla/models/docusign_models.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: MIT """ -Easily perform signing workflows using DocuSign signing service with docusign_esign. +Easily perform signing workflows using DocuSign signing service with pydocusign. NOTE: This integration uses DocuSign's Legacy Authentication REST API Integration. https://developers.docusign.com/esign-rest-api/guides/post-go-live @@ -14,31 +14,30 @@ import os import urllib.request import uuid -import base64 import xml.etree.ElementTree as ET from typing import Any, Dict, List, Optional from urllib.parse import urlparse import cla +import pydocusign # type: ignore import requests from attr import dataclass from cla.controllers.lf_group import LFGroup from cla.models import DoesNotExist, signing_service_interface from cla.models.dynamo_models import (Company, Document, Event, Gerrit, Project, Signature, User) -import docusign_esign -from docusign_esign.client.api_exception import ApiException from cla.models.event_types import EventType from cla.models.s3_storage import S3Storage from cla.user_service import UserService from cla.utils import (append_email_help_sign_off_content, get_corporate_url, get_email_help_content, get_project_cla_group_instance) +from pydocusign.exceptions import DocuSignException # type: ignore api_base_url = os.environ.get('CLA_API_BASE', '') - -ds_user_id = os.environ.get('DOCUSIGN_USER_ID', '') -ds_auth_url = os.environ.get('DOCUSIGN_AUTH_SERVER', '') -ds_client_id = os.environ.get('DOCUSIGN_INTEGRATOR_KEY', '') +root_url = os.environ.get('DOCUSIGN_ROOT_URL', '') +username = os.environ.get('DOCUSIGN_USERNAME', '') +password = os.environ.get('DOCUSIGN_PASSWORD', '') +integrator_key = os.environ.get('DOCUSIGN_INTEGRATOR_KEY', '') lf_group_client_url = os.environ.get('LF_GROUP_CLIENT_URL', '') lf_group_client_id = os.environ.get('LF_GROUP_CLIENT_ID', '') @@ -97,49 +96,35 @@ class DocuSign(signing_service_interface.SigningService): 'signed_date': '{http://www.docusign.net/API/3.0}Signed', } - SCOPES = [ - "signature", "impersonation" - ] def __init__(self): self.client = None - self.ds_account_id = "" self.s3storage = None - def get_private_key(self): - return cla.config.DOCUSIGN_PRIVATE_KEY - def initialize(self, config): - api_client = docusign_esign.ApiClient() - api_client.set_base_path(ds_auth_url) - api_client.set_oauth_host_name(ds_auth_url) - + self.client = pydocusign.DocuSignClient(root_url=root_url, + username=username, + password=password, + integrator_key=integrator_key) + try: - ds_private_key = self.get_private_key() - token_response = api_client.request_jwt_user_token( - client_id=ds_client_id, - user_id=ds_user_id, - oauth_host_name=ds_auth_url, - private_key_bytes=ds_private_key, - expires_in=4000, - scopes=self.SCOPES - ) - if token_response.access_token != None and token_response.access_token!= "": - user_info = api_client.get_user_info(token_response.access_token) - accounts = user_info.get_accounts() - ds_base_url = accounts[0].base_uri + "/restapi" - self.ds_account_id = accounts[0].account_id - api_client.host = ds_base_url - api_client.set_default_header(header_name="Authorization", header_value=f"Bearer {token_response.access_token}") - self.client = api_client - else: - return {'errors': {'Error initializing DocuSign'}} + login_data = self.client.login_information() + login_account = login_data['loginAccounts'][0] + base_url = login_account['baseUrl'] + account_id = login_account['accountId'] + url = urlparse(base_url) + parsed_root_url = '{}://{}/restapi/v2'.format(url.scheme, url.netloc) except Exception as e: - cla.log.error('could not gnerate access_token: {}'.format(e)) + cla.log.error('Error logging in to DocuSign: {}'.format(e)) return {'errors': {'Error initializing DocuSign'}} - + + self.client = pydocusign.DocuSignClient(root_url=parsed_root_url, + account_url=base_url, + account_id=account_id, + username=username, + password=password, + integrator_key=integrator_key) self.s3storage = S3Storage() self.s3storage.initialize(None) - return None def request_individual_signature(self, project_id, user_id, return_url=None, return_url_type="github", callback_url=None, preferred_email=None): @@ -1308,15 +1293,7 @@ def populate_sign_url(self, signature, callback_url=None, f'for project {project.get_project_name()} expired. A new session will be in place for ' 'your signing process.') cla.log.debug(message) - env = docusign_esign.Envelope() - env.status = 'voided' - env.voided_reason = message - envelope_api = docusign_esign.EnvelopesApi(self.client) - envelope_api.update( - account_id=self.ds_account_id, - envelope_id=envelope_id, - envelope=env - ) + self.client.void_envelope(envelope_id, message) except Exception as e: cla.log.warning(f'{fn} - {sig_type} - DocuSign error while voiding the envelope - ' f'regardless, continuing on..., error: {e}') @@ -1324,7 +1301,7 @@ def populate_sign_url(self, signature, callback_url=None, # Not sure what should be put in as documentId. document_id = uuid.uuid4().int & (1 << 16) - 1 # Random 16bit integer -.pylint: disable=no-member tabs = get_docusign_tabs_from_document(document, document_id, default_values=default_values) - + if send_as_email: cla.log.warning(f'{fn} - {sig_type} - assigning signatory name/email: ' f'{authority_or_signatory_name} / {authority_or_signatory_email}') @@ -1356,15 +1333,14 @@ def populate_sign_url(self, signature, callback_url=None, project_names=project_names)) cla.log.debug(f'populate_sign_url - {sig_type} - generating a docusign signer object form email with' f'name: {signatory_name}, email: {signatory_email}, subject: {email_subject}') - signer = docusign_esign.Signer(email=signatory_email, + signer = pydocusign.Signer(email=signatory_email, name=signatory_name, - recipient_id=1, + recipientId=1, tabs=tabs, - email_notification = { - 'emailBody': email_body, - 'emailSubject': email_subject, - 'supportedLanguage': 'en', - }) + emailSubject=email_subject, + emailBody=email_body, + supportedLanguage='en', + ) else: # This will be the Initial CLA Manager signatory_name = user_signature_name @@ -1388,14 +1364,13 @@ def populate_sign_url(self, signature, callback_url=None, user_identifier = signatory_email else: user_identifier = signatory_name - signer = docusign_esign.Signer(email=signatory_email, name=signatory_name, - recipient_id=1, client_user_id=signature.get_signature_id(), + signer = pydocusign.Signer(email=signatory_email, name=signatory_name, + recipientId=1, clientUserId=signature.get_signature_id(), tabs=tabs, - email_notification = { - 'emailBody': 'CLA Sign Request for {}'.format(user_identifier), - 'emailSubject': email_subject, - 'supportedLanguage': 'en', - }) + emailSubject=email_subject, + emailBody='CLA Sign Request for {}'.format(user_identifier), + supportedLanguage='en', + ) content_type = document.get_document_content_type() if document.get_document_s3_url() is not None: @@ -1406,52 +1381,53 @@ def populate_sign_url(self, signature, callback_url=None, else: content = document.get_document_content() pdf = io.BytesIO(content) - content_bytes = pdf.read() - base64_file_content = base64.b64encode(content_bytes).decode("ascii") + doc_name = document.get_document_name() cla.log.debug(f'{fn} - {sig_type} - docusign document ' f'name: {doc_name}, id: {document_id}, content type: {content_type}') - document = docusign_esign.Document(name=doc_name, document_id=document_id, document_base64=base64_file_content, file_extension="pdf") + document = pydocusign.Document(name=doc_name, documentId=document_id, data=pdf) if callback_url is not None: # Webhook properties for callbacks after the user signs the document. # Ensure that a webhook is returned on the status "Completed" where # all signers on a document finish signing the document. recipient_events = [{"recipientEventStatusCode": "Completed"}] - event_notification = docusign_esign.EventNotification(url=callback_url, - logging_enabled=True, - recipient_events=recipient_events) - envelope = docusign_esign.EnvelopeDefinition( + event_notification = pydocusign.EventNotification(url=callback_url, + loggingEnabled=True, + recipientEvents=recipient_events) + envelope = pydocusign.Envelope( documents=[document], - email_subject=f'EasyCLA: CLA Signature Request for {project.get_project_name()}', - email_blurb='CLA Sign Request', - event_notification=event_notification, - status="sent", - recipients=docusign_esign.Recipients(signers=[signer])) + emailSubject=f'EasyCLA: CLA Signature Request for {project.get_project_name()}', + emailBlurb='CLA Sign Request', + eventNotification=event_notification, + status=pydocusign.Envelope.STATUS_SENT, + recipients=[signer]) else: - envelope = docusign_esign.EnvelopeDefinition( + envelope = pydocusign.Envelope( documents=[document], - email_subject=f'EasyCLA: CLA Signature Request for {project.get_project_name()}', - email_blurb='CLA Sign Request', - status="sent", - recipients=docusign_esign.Recipients(signers=[signer])) - envelope_result = self.prepare_sign_request(envelope) + emailSubject=f'EasyCLA: CLA Signature Request for {project.get_project_name()}', + emailBlurb='CLA Sign Request', + status=pydocusign.Envelope.STATUS_SENT, + recipients=[signer]) + + envelope = self.prepare_sign_request(envelope) if not send_as_email: + recipient = envelope.recipients[0] + # The URL the user will be redirected to after signing. # This route will be in charge of extracting the signature's return_url and redirecting. - return_url = os.path.join(api_base_url, 'v2/return-url', str(signer.client_user_id)) + return_url = os.path.join(api_base_url, 'v2/return-url', str(recipient.clientUserId)) cla.log.debug(f'populate_sign_url - {sig_type} - generating signature sign_url, ' f'using return-url as: {return_url}') - sign_url = self.get_sign_url(envelope_result.envelope_id, signer, return_url) + sign_url = self.get_sign_url(envelope, recipient, return_url) cla.log.debug(f'populate_sign_url - {sig_type} - setting signature sign_url as: {sign_url}') - signature.set_signature_sign_url(sign_url.url) + signature.set_signature_sign_url(sign_url) # Save Envelope ID in signature. - cla.log.debug(f'{fn} - {sig_type} - saving signature to database...') - signature.set_signature_envelope_id(envelope_result.envelope_id) + signature.set_signature_envelope_id(envelope.envelopeId) signature.save() cla.log.debug(f'{fn} - {sig_type} - saved signature to database - id: {signature.get_signature_id()}...') cla.log.debug(f'populate_sign_url - {sig_type} - complete') @@ -1921,10 +1897,11 @@ def get_signed_document(self, envelope_id, user): fn = 'models.docusign_models.get_signed_document' cla.log.debug(f'{fn} - fetching signed CLA document for envelope: {envelope_id}') - + envelope = pydocusign.Envelope() + envelope.envelopeId = envelope_id + try: - envelope_api = docusign_esign.EnvelopesApi(self.client) - documents = envelope_api.list_documents(account_id=self.ds_account_id, envelope_id=envelope_id) + documents = envelope.get_document_list(self.client) except Exception as err: cla.log.error(f'{fn} - unknown error when trying to load signed document: {err}') return @@ -1935,17 +1912,17 @@ def get_signed_document(self, envelope_id, user): return document = documents[0] - if 'document_id' not in document: + if 'documentId' not in document: cla.log.error(f'{fn} - not document ID found in document response: {document}') return try: # TODO: Also send the signature certificate? envelope.get_certificate() - document_file = envelope_api.get_document(account_id=self.ds_account_id,document_id=document['document_id'],envelope_id=envelope_id) + document_file = envelope.get_document(document['documentId'], self.client) return document_file.read() except Exception as err: cla.log.error('{fn} - unknown error when trying to fetch signed document content ' - f'for document ID {document["document_id"]}, error: {err}') + f'for document ID {document["documentId"]}, error: {err}') return def send_signed_document(self, signature, document_data, user, icla=True): @@ -1992,49 +1969,37 @@ def get_document_resource(self, url): # pylint: disable=no-self-use """ return urllib.request.urlopen(url) - def prepare_sign_request(self, envelope_definition): + def prepare_sign_request(self, envelope): """ Mockable method for sending a signature request to DocuSign. :param envelope: The envelope to send to DocuSign. - :type envelope: docusign_esign.Envelope + :type envelope: pydocusign.Envelope :return: The new envelope to work with after the request has been sent. - :rtype: docusign_esign.Envelope + :rtype: pydocusign.Envelope """ try: - envelope_api = docusign_esign.EnvelopesApi(self.client) - result = envelope_api.create_envelope(account_id=self.ds_account_id, envelope_definition=envelope_definition) - return result - except ApiException as err: + self.client.create_envelope_from_documents(envelope) + envelope.get_recipients() + return envelope + except DocuSignException as err: cla.log.error(f'prepare_sign_request - error while fetching DocuSign envelope recipients: {err}') - def get_sign_url(self, envelope_id, recipient, return_url): # pylint:disable=no-self-use + def get_sign_url(self, envelope, recipient, return_url): # pylint:disable=no-self-use """ Mockable method for getting a signing url. :param envelope: The envelope in question. - :type envelope: docusign_esign.Envelope + :type envelope: pydocusign.Envelope :param recipient: The recipient inside this envelope. - :type recipient: docusign_esign.Recipient + :type recipient: pydocusign.Recipient :param return_url: The URL to return the user after successful signing. :type return_url: string :return: A URL for the recipient to hit for signing. :rtype: string """ - recipient_view_request = docusign_esign.RecipientViewRequest( - authentication_method="None", - client_user_id=recipient.client_user_id, - recipient_id="1", - return_url=return_url, - user_name=recipient.name, - email=recipient.email - ) - envelope_api = docusign_esign.EnvelopesApi(self.client) - return envelope_api.create_recipient_view( - account_id=self.ds_account_id, - envelope_id=envelope_id, - recipient_view_request=recipient_view_request - ) + return envelope.post_recipient_view(recipient, returnUrl=return_url) + class MockDocuSign(DocuSign): """ @@ -2108,7 +2073,6 @@ def get_org_from_return_url(repo_provider_type, return_url, orgs): raise Exception('Repo service: {} not supported'.format(repo_provider_type)) - def get_docusign_tabs_from_document(document: Document, document_id: int, default_values: Optional[Dict[str, Any]] = None): @@ -2119,167 +2083,70 @@ def get_docusign_tabs_from_document(document: Document, :type document: cla.models.model_interfaces.Document :param document_id: The ID of the document to use for grouping of the tabs. :type document_id: int - :return: List of formatted tabs for consumption by docusign_esign. - :rtype: [docusign_esign.Tabs] + :return: List of formatted tabs for consumption by pydocusign. + :rtype: [pydocusign.Tab] """ - text_tabs = [] - num_tabs = [] - sign_here_tabs = [] - date_tabs = []; + tabs = [] for tab in document.get_document_tabs(): - + args = { + 'documentId': document_id, + 'pageNumber': tab.get_document_tab_page(), + 'xPosition': tab.get_document_tab_position_x(), + 'yPosition': tab.get_document_tab_position_y(), + 'width': tab.get_document_tab_width(), + 'height': tab.get_document_tab_height(), + 'customTabId': tab.get_document_tab_id(), + 'tabLabel': tab.get_document_tab_id(), + 'name': tab.get_document_tab_name() + } + + if tab.get_document_tab_anchor_string() is not None: + # Set only when anchor string exists + args['anchorString'] = tab.get_document_tab_anchor_string() + args['anchorIgnoreIfNotPresent'] = tab.get_document_tab_anchor_ignore_if_not_present() + args['anchorXOffset'] = tab.get_document_tab_anchor_x_offset() + args['anchorYOffset'] = tab.get_document_tab_anchor_y_offset() + # Remove x,y coordinates since offsets will define them + # del args['xPosition'] + # del args['yPosition'] + + if default_values is not None and \ + default_values.get(tab.get_document_tab_id()) is not None: + args['value'] = default_values[tab.get_document_tab_id()] + tab_type = tab.get_document_tab_type() if tab_type == 'text': - text_tab = docusign_esign.Text( - document_id=document_id, - page_number=tab.get_document_tab_page(), - x_position = tab.get_document_tab_position_x(), - y_position = tab.get_document_tab_position_y(), - width= tab.get_document_tab_width(), - height= tab.get_document_tab_height(), - custom_tab_id= tab.get_document_tab_id(), - tab_label= tab.get_document_tab_id(), - name=tab.get_document_tab_name() - ) - if tab.get_document_tab_anchor_string() is not None: - text_tab.anchor_string = tab.get_document_tab_anchor_string() - text_tab.anchor_ignore_if_not_present = tab.get_document_tab_anchor_ignore_if_not_present() - text_tab.anchor_x_offset = tab.get_document_tab_anchor_x_offset() - text_tab.anchor_y_offset = tab.get_document_tab_anchor_y_offset() - if default_values is not None and default_values.get(tab.get_document_tab_id()) is not None: - text_tab.value = default_values[tab.get_document_tab_id()] - text_tabs.append(text_tab) + tab_class = pydocusign.TextTab elif tab_type == 'text_unlocked': - text_tab = docusign_esign.Text( - document_id=document_id, - page_number=tab.get_document_tab_page(), - x_position = tab.get_document_tab_position_x(), - y_position = tab.get_document_tab_position_y(), - width= tab.get_document_tab_width(), - height= tab.get_document_tab_height(), - custom_tab_id= tab.get_document_tab_id(), - tab_label= tab.get_document_tab_id(), - name=tab.get_document_tab_name(), - locked=False - ) - if tab.get_document_tab_anchor_string() is not None: - text_tab.anchor_string = tab.get_document_tab_anchor_string() - text_tab.anchor_ignore_if_not_present = tab.get_document_tab_anchor_ignore_if_not_present() - text_tab.anchor_x_offset = tab.get_document_tab_anchor_x_offset() - text_tab.anchor_y_offset = tab.get_document_tab_anchor_y_offset() - if default_values is not None and default_values.get(tab.get_document_tab_id()) is not None: - text_tab.value = default_values[tab.get_document_tab_id()] - text_tabs.append(text_tab) + tab_class = TextUnlockedTab + args['locked'] = False elif tab_type == 'text_optional': - text_tab = docusign_esign.Text( - document_id=document_id, - page_number=tab.get_document_tab_page(), - x_position = tab.get_document_tab_position_x(), - y_position = tab.get_document_tab_position_y(), - width= tab.get_document_tab_width(), - height= tab.get_document_tab_height(), - custom_tab_id= tab.get_document_tab_id(), - tab_label= tab.get_document_tab_id(), - name=tab.get_document_tab_name(), - required=False - ) - if tab.get_document_tab_anchor_string() is not None: - text_tab.anchor_string = tab.get_document_tab_anchor_string() - text_tab.anchor_ignore_if_not_present = tab.get_document_tab_anchor_ignore_if_not_present() - text_tab.anchor_x_offset = tab.get_document_tab_anchor_x_offset() - text_tab.anchor_y_offset = tab.get_document_tab_anchor_y_offset() - if default_values is not None and default_values.get(tab.get_document_tab_id()) is not None: - text_tab.value = default_values[tab.get_document_tab_id()] - text_tabs.append(text_tab) + tab_class = TextOptionalTab + # https://developers.docusign.com/docs/esign-rest-api/reference/envelopes/enveloperecipienttabs/create/#schema__enveloperecipienttabs_texttabs_required + # required: string - When true, the signer is required to fill out this tab. + args['required'] = False elif tab_type == 'number': - num_tab = docusign_esign.Number( - document_id=document_id, - page_number=tab.get_document_tab_page(), - x_position = tab.get_document_tab_position_x(), - y_position = tab.get_document_tab_position_y(), - width= tab.get_document_tab_width(), - height= tab.get_document_tab_height(), - custom_tab_id= tab.get_document_tab_id(), - tab_label= tab.get_document_tab_id(), - name=tab.get_document_tab_name(), - ) - if tab.get_document_tab_anchor_string() is not None: - num_tab.anchor_string = tab.get_document_tab_anchor_string() - num_tab.anchor_ignore_if_not_present = tab.get_document_tab_anchor_ignore_if_not_present() - num_tab.anchor_x_offset = tab.get_document_tab_anchor_x_offset() - num_tab.anchor_y_offset = tab.get_document_tab_anchor_y_offset() - if default_values is not None and default_values.get(tab.get_document_tab_id()) is not None: - num_tab.value = default_values[tab.get_document_tab_id()] - num_tabs.append(num_tab) + tab_class = pydocusign.NumberTab elif tab_type == 'sign': - sign_here_tab = docusign_esign.SignHere( - document_id=document_id, - page_number=tab.get_document_tab_page(), - x_position = tab.get_document_tab_position_x(), - y_position = tab.get_document_tab_position_y(), - width= tab.get_document_tab_width(), - height= tab.get_document_tab_height(), - custom_tab_id= tab.get_document_tab_id(), - tab_label= tab.get_document_tab_id(), - name=tab.get_document_tab_name() - ) - - if tab.get_document_tab_anchor_string() is not None: - sign_here_tab.anchor_string = tab.get_document_tab_anchor_string() - sign_here_tab.anchor_ignore_if_not_present = tab.get_document_tab_anchor_ignore_if_not_present() - sign_here_tab.anchor_x_offset = tab.get_document_tab_anchor_x_offset() - sign_here_tab.anchor_y_offset = tab.get_document_tab_anchor_y_offset() - sign_here_tabs.append(sign_here_tab) + tab_class = pydocusign.SignHereTab elif tab_type == 'sign_optional': - sign_here_tab = docusign_esign.SignHere( - document_id=document_id, - page_number=tab.get_document_tab_page(), - x_position = tab.get_document_tab_position_x(), - y_position = tab.get_document_tab_position_y(), - width= tab.get_document_tab_width(), - height= tab.get_document_tab_height(), - custom_tab_id= tab.get_document_tab_id(), - tab_label= tab.get_document_tab_id(), - name=tab.get_document_tab_name(), - Optional=True - ) - if tab.get_document_tab_anchor_string() is not None: - sign_here_tab.anchor_string = tab.get_document_tab_anchor_string() - sign_here_tab.anchor_ignore_if_not_present = tab.get_document_tab_anchor_ignore_if_not_present() - sign_here_tab.anchor_x_offset = tab.get_document_tab_anchor_x_offset() - sign_here_tab.anchor_y_offset = tab.get_document_tab_anchor_y_offset() - sign_here_tabs.append(sign_here_tab) + tab_class = pydocusign.SignHereTab + # https://developers.docusign.com/docs/esign-rest-api/reference/envelopes/enveloperecipienttabs/create/#schema__enveloperecipienttabs_signheretabs_optional + # optional: string - When true, the recipient does not need to complete this tab to + # complete the signing process. + args['optional'] = True elif tab_type == 'date': - date_tab = docusign_esign.DateSigned( - document_id=document_id, - page_number=tab.get_document_tab_page(), - x_position = tab.get_document_tab_position_x(), - y_position = tab.get_document_tab_position_y(), - width= tab.get_document_tab_width(), - height= tab.get_document_tab_height(), - custom_tab_id= tab.get_document_tab_id(), - tab_label= tab.get_document_tab_id(), - name=tab.get_document_tab_name() - ) - if tab.get_document_tab_anchor_string() is not None: - date_tab.anchor_string = tab.get_document_tab_anchor_string() - date_tab.anchor_ignore_if_not_present = tab.get_document_tab_anchor_ignore_if_not_present() - date_tab.anchor_x_offset = tab.get_document_tab_anchor_x_offset() - date_tab.anchor_y_offset = tab.get_document_tab_anchor_y_offset() - if default_values is not None and default_values.get(tab.get_document_tab_id()) is not None: - date_tab.value = default_values[tab.get_document_tab_id()] - date_tabs.append(date_tab) + tab_class = pydocusign.DateSignedTab else: cla.log.warning('Invalid tab type specified (%s) in document file ID %s', tab_type, document.get_document_file_id()) continue - - return docusign_esign.Tabs( - sign_here_tabs=sign_here_tabs, - text_tabs=text_tabs, - date_signed_tabs=date_tabs, - number_tabs=num_tabs - ) + + tab_obj = tab_class(**args) + tabs.append(tab_obj) + + return tabs + def populate_signature_from_icla_callback(content: str, icla_tree: ET, signature: Signature): """ @@ -2431,6 +2298,34 @@ def create_default_individual_values(user: User, preferred_email: str = None) -> return values + +class TextOptionalTab(pydocusign.Tab): + """Tab to show a free-form text field on the document. + """ + attributes = pydocusign.Tab._common_attributes + pydocusign.Tab._formatting_attributes + [ + 'name', + 'value', + 'height', + 'width', + 'locked', + 'required' + ] + tabs_name = 'textTabs' + + +class TextUnlockedTab(pydocusign.Tab): + """Tab to show a free-form text field on the document. + """ + attributes = pydocusign.Tab._common_attributes + pydocusign.Tab._formatting_attributes + [ + 'name', + 'value', + 'height', + 'width', + 'locked' + ] + tabs_name = 'textTabs' + + # managers and contributors are tuples of (name, email) def generate_manager_and_contributor_list(managers, contributors=None): lines = [] diff --git a/cla-backend/cla/utils.py b/cla-backend/cla/utils.py index 0fa66d154..cfbabcfc6 100644 --- a/cla-backend/cla/utils.py +++ b/cla-backend/cla/utils.py @@ -346,9 +346,7 @@ def get_signing_service(conf=None, initialize=True): raise Exception('Invalid signing service selected in configuration: %s' % signing_service) signing_service_instance = signing() if initialize: - resp = signing_service_instance.initialize(conf) - if resp != None: - raise Exception('docusign authentication error : %s' % resp["errors"]) + signing_service_instance.initialize(conf) return signing_service_instance diff --git a/cla-backend/requirements.txt b/cla-backend/requirements.txt index 4e102bfcb..d4c5f7658 100644 --- a/cla-backend/requirements.txt +++ b/cla-backend/requirements.txt @@ -33,6 +33,7 @@ packaging==20.5 pluggy==0.13.1 py==1.10.0 pyasn1==0.4.8 +pydocusign==2.2 PyGithub==1.55 PyJWT==2.7.0 pylint==1.5.2 @@ -58,5 +59,4 @@ wcwidth==0.1.7 Werkzeug==0.15.5 wrapt==1.11.2 zipp==3.15.0 -markupsafe==2.0.1 -docusign-esign==3.22.0 \ No newline at end of file +markupsafe==2.0.1 \ No newline at end of file diff --git a/cla-backend/serverless.yml b/cla-backend/serverless.yml index 3e2385869..fc35ff83d 100644 --- a/cla-backend/serverless.yml +++ b/cla-backend/serverless.yml @@ -318,8 +318,9 @@ provider: SF_USERNAME: ${file(./env.json):sf-username, ssm:/cla-sf-username-${sls:stage}} SF_PASSWORD: ${file(./env.json):sf-password, ssm:/cla-sf-password-${sls:stage}} DOCRAPTOR_API_KEY: ${file(./env.json):doc-raptor-api-key, ssm:/cla-doc-raptor-api-key-${sls:stage}} - DOCUSIGN_AUTH_SERVER: ${file(./env.json):docusign-auth-server, ssm:/cla-docusign-auth-server-${sls:stage}} - DOCUSIGN_USER_ID: ${file(./env.json):docusign-user-id, ssm:/cla-docusign-user-id-${sls:stage}} + DOCUSIGN_ROOT_URL: ${file(./env.json):docusign-root-url, ssm:/cla-docusign-root-url-${sls:stage}} + DOCUSIGN_USERNAME: ${file(./env.json):docusign-username, ssm:/cla-docusign-username-${sls:stage}} + DOCUSIGN_PASSWORD: ${file(./env.json):docusign-password, ssm:/cla-docusign-password-${sls:stage}} DOCUSIGN_INTEGRATOR_KEY: ${file(./env.json):docusign-integrator-key, ssm:/cla-docusign-integrator-key-${sls:stage}} CLA_API_BASE: ${file(./env.json):cla-api-base, ssm:/cla-api-base-${sls:stage}} CLA_CONTRIBUTOR_BASE: ${file(./env.json):cla-contributor-base, ssm:/cla-contributor-base-${sls:stage}}