From b062627e8f52da7e5a2b7e3733445c9feba3821f Mon Sep 17 00:00:00 2001 From: Harold Wanyama Date: Tue, 10 Oct 2023 19:32:28 +0300 Subject: [PATCH] [#4002] Docusign API - Golang - Moved from Python to Golang API v2 for icla and ccla signature requests Signed-off-by: Harold Wanyama --- cla-backend-go/swagger/cla.v2.yaml | 36 +-- cla-backend-go/v2/docusign_auth/auth.go | 102 --------- cla-backend-go/v2/main/main.go | 47 ---- cla-backend-go/v2/project/handlers.go | 2 +- cla-backend-go/v2/sign/docusign.go | 78 ++++++- cla-backend-go/v2/sign/handlers.go | 9 +- cla-backend-go/v2/sign/jwt.go | 51 ++++- cla-backend-go/v2/sign/models.go | 292 ++++++++++++++++++++++++ cla-backend-go/v2/sign/service.go | 22 +- 9 files changed, 439 insertions(+), 200 deletions(-) delete mode 100644 cla-backend-go/v2/docusign_auth/auth.go delete mode 100644 cla-backend-go/v2/main/main.go create mode 100644 cla-backend-go/v2/sign/models.go diff --git a/cla-backend-go/swagger/cla.v2.yaml b/cla-backend-go/swagger/cla.v2.yaml index 64eb14c05..947e5ebaf 100644 --- a/cla-backend-go/swagger/cla.v2.yaml +++ b/cla-backend-go/swagger/cla.v2.yaml @@ -4137,7 +4137,7 @@ paths: - name: input in: body schema: - $ref: '#/definitions/icla-signature-input' + $ref: '#/definitions/individual-signature-input' required: true responses: '200': @@ -5527,38 +5527,26 @@ definitions: corporate-contributor: $ref: './common/corporate-contributor.yaml' - icla-signature-input: + individual-signature-input: type: object required: - - project_sfid - - company_sfid + - project_id + - user_id properties: - project_sfid: - type: string - example: 'a0941000005ouJFAAY' - description: salesforce id of the project - company_sfid: - type: string - example: '0014100000Te0fMAAR' - description: salesforce id of the company - send_as_email: - type: boolean - example: false - description: send signing request as email. This should be set to true when requestor is not signatory. - authority_name: + project_id: type: string - example: "Derk Miyamoto" - description: the name of the CLA signatory - minLength: 2 - maxLength: 255 - authority_email: - $ref: './common/properties/email.yaml' - description: the email of the CLA Signatory + example: "e1e30240-a722-4c82-a648-121681d959c7" return_url: type: string example: 'https://corporate.dev.lfcla.com/#/company/eb4d7d71-693f-4047-bf8d-10d0e7764969' description: on signing the document, page will get redirected to this url. This is valid only when send_as_email is false format: uri + return_url_type: + type: string + example: Gerrit/Github/GitLab. Optional depending on presence of return_url + user_id: + type: string + example: "e1e30240-a722-4c82-a648-121681d959c7" corporate-signature-input: diff --git a/cla-backend-go/v2/docusign_auth/auth.go b/cla-backend-go/v2/docusign_auth/auth.go deleted file mode 100644 index 72f1e6bd4..000000000 --- a/cla-backend-go/v2/docusign_auth/auth.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -package docusignauth - -import ( - "crypto/x509" - "encoding/json" - "encoding/pem" - "fmt" - "io" - "net/http" - "os" - "strings" - "time" - - jwt "github.com/dgrijalva/jwt-go" -) - -var ( - baseURL = os.Getenv("DOCUSIGN_AUTH_SERVER") - oauthTokenURL = baseURL + "/oauth/token" - jwtGrantAssertion = "urn:ietf:params:oauth:grant-type:jwt-bearer" -) - -type TokenResponse struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` -} - -func GetAccessToken(integrationKey, userGUID, privateKey string) (string, error) { - block, _ := pem.Decode([]byte(privateKey)) - if block == nil { - return "", fmt.Errorf("failed to parse private key") - } - - privateKeyParsed, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return "", err - } - - // Generate the JWT token - tokenString, err := generateJWT(integrationKey, userGUID, privateKeyParsed.D.String()) - if err != nil { - return "", err - } - - // Make the HTTP request to get the access token - body := strings.NewReader(fmt.Sprintf("grant_type=%s&assertion=%s", jwtGrantAssertion, tokenString)) - req, err := http.NewRequest("POST", oauthTokenURL, body) - if err != nil { - return "", err - } - - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() // nolint - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("failed to get access token, status: %s", resp.Status) - } - - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - var tokenResponse TokenResponse - if err := json.Unmarshal(respBody, &tokenResponse); err != nil { - return "", err - } - - return tokenResponse.AccessToken, nil -} - -func generateJWT(integrationKey, userGUID, privateKey string) (string, error) { - token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ - "iss": integrationKey, // Integration Key - "sub": userGUID, // User GUID - "aud": baseURL, // Base URL - "iat": time.Now().Unix(), // Issued At - "exp": time.Now().Add(1 * time.Hour).Unix(), // Expiration time - 1 hour is recommended - "scope": "signature", // Permission scope - }) - - signKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(privateKey)) - if err != nil { - return "", err - } - - tokenString, err := token.SignedString(signKey) - if err != nil { - return "", err - } - - return tokenString, nil -} diff --git a/cla-backend-go/v2/main/main.go b/cla-backend-go/v2/main/main.go deleted file mode 100644 index d220a74c4..000000000 --- a/cla-backend-go/v2/main/main.go +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/communitybridge/easycla/cla-backend-go/v2/docusign_auth" -) - -func main() { - integrationKey := "557677f2-1e2f-4955-aaa6-1ef44630e01d" - userID := "3a1d118f-3083-4c25-8306-11b7400f7c03" - privateKey :=` - -----BEGIN RSA PRIVATE KEY----- - MIIEogIBAAKCAQEAh0M2mIGaJjP8S/FxZR7nRsatCR/KpCPBFBbxalZffykqtTID - KNeDhJ5RvJKAVoJlLaLoUYSYloVaeSAwQdbn4F+Lsnll3mCGocwdl/W8998Lc/Ln - MaNQhpekBoXaq8vbj251jxnRcsdI9yl/YyQo3jZnM77OWtEF7dyvS6V9cMprT2Ca - eIJPj0Ck3/P9rGwE3DdiEXZDetgkuNQyMavfvLKltCNu3qQXFA95PXlHs2E57OrS - CNIQT37jfInXuIyCoGjDSq+U3ZE047UG5Id8/OFvcP1z5iUdwvueoEximt/kSvR0 - ZcNfjqyJnnYq80OKwTdalZEOzAEkt/U2QuB9jwIDAQABAoIBAB01GstopOgd7ptZ - efJrb2ZdjUy8lC3IWK9lWuDq4LkdIw84Su1dSBVxeFXfTp4fjwiBNmgv2SEbj5M7 - K6Bz7uMIzqoNw7z2m+vBHxzKn/DoNVlmuJyD1uYRRYZxDext2y3IHNN3MD54IN3a - FJtMWhTNq5BFYdrDauPXdPTBOeqKQgMoQl6OwHIx4WJXYxRgIgrvBYRzLSwG/u4V - tAvj1/J0PSSOY5A1qn6L2ii9abVElFr2zZC5MtG3oG1TLCidzxZFt/5qSHGo+0tU - l/7YRXWxadfYrPmS9akbC7SQn9WJQdIlfejtuQ6hmKTCCjhTp2BUba3kTI3PcM46 - GzW3WQECgYEA08lwWFZng3bm3KrDx3ljjZ+cuL8vOXfLeYuaIPmNu5eXrR5ZR9Ar - UyUmnZbL8rhK9dNCmRGX293uuppwJolfMAy+kcvQ4HsRQ5tOgXmP8VhKzA4RRc+h - I7uIGQ7NmKI6Wm/wJ+T3Okw0h8ze8MW66/D5IHDQSoGQZH9kkA9+MQECgYEAo4AS - jB/b/SWhwrRUI8m+5Tjy5WO9ZkRZ54zpRnAr5HsjSBgb72IGbJKwleKwb3FxwPTQ - ZlgKP2O5rGzZOEZcIryIliCpV4C/XeFVwUKxo/A5jkL+U9ZQwoA0/0ssdE/7uLzG - FiKcFK7OhsFg+dNEX1ForM9cGN5OzY8sGzylHo8CgYAc0Nq1WkRJUeNFgQKUYILY - ITB8vp6ZTiBkUEdPV0Uekhi0GF4DdGKAtJxVctAbHVItsmnsU8V6x+6UezDpPWWz - Lvi686Ve9b+6mCYNXdHk/6NlskBNZFvDdd+lsSruKpyP840UkIXG69l15L0su2qc - cbQj4tWkXY6c7exr4X/FAQKBgDmGTABFDU9puBoa/CeDSci4Wq1ehDrA/ai8KS8B - NFA1CtrIsLtuj7gPfFWf5levYEh1WgVIIILhAWiq+1oTV0NZdezsHOiOgcX0DAns - /zcgw/9LjtPMaamlFgBkYIWjxnre4ArVrniQcFV1IDuFm16189ApPMv7G1qzbt8+ - XRH9AoGAceSrZLbceDkqgprTTV1BEOFY+Ti9edBIUW+CqN6KkB66OXHGWlab/PZc - OCHRPAjEijZcVXm8IPC2yi/s0agQAB8dKO2L1X0EtvxkWSg2s/YXFpp0QccQToTo - lRFb9injNzixUOq18Z62XcC/yqMB7QtgRw5x5OQk0HNWpL7h6KM= - -----END RSA PRIVATE KEY----- - ` - token, err := docusignauth.GetAccessToken(integrationKey, userID, privateKey) - if err != nil { - fmt.Println(err) - } - - fmt.Println(token) -} \ No newline at end of file diff --git a/cla-backend-go/v2/project/handlers.go b/cla-backend-go/v2/project/handlers.go index f936bdebe..369c7c165 100644 --- a/cla-backend-go/v2/project/handlers.go +++ b/cla-backend-go/v2/project/handlers.go @@ -338,7 +338,7 @@ func buildSFProjectSummary(sfProject *v2ProjectServiceModels.ProjectOutputDetail return &models.SfProjectSummary{ EntityName: utils.StringValue(sfProject.EntityName), EntityType: sfProject.EntityType, - Funding: sfProject.Funding, + Funding: *sfProject.Funding, ID: sfProject.ID, LfSupported: sfProject.LFSponsored, Name: sfProject.Name, diff --git a/cla-backend-go/v2/sign/docusign.go b/cla-backend-go/v2/sign/docusign.go index 394a332a6..da465f4de 100644 --- a/cla-backend-go/v2/sign/docusign.go +++ b/cla-backend-go/v2/sign/docusign.go @@ -5,14 +5,82 @@ package sign import ( "context" - "log" + "encoding/json" + "errors" + "io" + "net/http" + "strings" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/sirupsen/logrus" ) +// getAccessToken retrieves an access token for the DocuSign API using a JWT assertion. func (s *service) getAccessToken(ctx context.Context) (string, error) { f := logrus.Fields{ - "functionName": "sign.getAccessToken", + "functionName": "v2.getAccessToken", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + jwtAssertion, err := jwtToken() + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem generating the JWT token") + return "", err } - // Get the access token - jwtAssertion, jwterr := jwtToken() -} \ No newline at end of file + // Create the request + tokenRequestBody := DocuSignGetTokenRequest{ + GrantType: "urn:ietf:params:oauth:grant-type:jwt-bearer", + Assertion: jwtAssertion, + } + + tokenRequestBodyJSON, err := json.Marshal(tokenRequestBody) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem marshalling the token request body") + return "", err + } + + url := utils.GetProperty("DOCUSIGN_AUTH_SERVER") + "/oauth/token" + req, err := http.NewRequest("POST", url, strings.NewReader(string(tokenRequestBodyJSON))) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem creating the HTTP request") + return "", err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + + // Make the request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem making the HTTP request") + return "", err + } + + defer resp.Body.Close() + + // Parse the response + responsePayload, err := io.ReadAll(resp.Body) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem reading the response body") + return "", err + } + + if resp.StatusCode != http.StatusOK { + log.WithFields(f).Warnf("problem making the HTTP request - status code: %d", resp.StatusCode) + return "", errors.New("problem making the HTTP request") + } + + var tokenResponse DocuSignGetTokenResponse + + err = json.Unmarshal(responsePayload, &tokenResponse) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem unmarshalling the response body") + return "", err + } + + return tokenResponse.AccessToken, nil + +} diff --git a/cla-backend-go/v2/sign/handlers.go b/cla-backend-go/v2/sign/handlers.go index d3c743acc..3518d5f58 100644 --- a/cla-backend-go/v2/sign/handlers.go +++ b/cla-backend-go/v2/sign/handlers.go @@ -21,7 +21,6 @@ import ( "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/communitybridge/easycla/cla-backend-go/v2/organization-service/client/organizations" "github.com/go-openapi/runtime/middleware" - ) // Configure API call @@ -85,10 +84,10 @@ func Configure(api *operations.EasyclaAPI, service Service) { f := logrus.Fields{ "functionName": "v2.sign.handlers.SignRequestIndividualSignatureHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "CompanyID": params.Input.CompanySfid, - "ProjectSFID": params.Input.ProjectSfid, - "authorityName": params.Input.AuthorityName, - "authorityEmail": params.Input.AuthorityEmail, + "projectID": params.Input.ProjectID, + "returnURL": params.Input.ReturnURL, + "returnURLType": params.Input.ReturnURLType, + "userID": params.Input.UserID, } log.WithFields(f).Debug("processing request") resp, err := service.RequestIndividualSignature(ctx, params.Input) diff --git a/cla-backend-go/v2/sign/jwt.go b/cla-backend-go/v2/sign/jwt.go index ac954d3e8..0eae4d856 100644 --- a/cla-backend-go/v2/sign/jwt.go +++ b/cla-backend-go/v2/sign/jwt.go @@ -4,14 +4,57 @@ package sign import ( + "time" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/golang-jwt/jwt" "github.com/sirupsen/logrus" ) -const - func jwtToken() (string, error) { + f := logrus.Fields{ + "functionName": "v2.sign.jwtToken", + } + claims := jwt.MapClaims{ - "iss": , + "iss": utils.GetProperty("DOCUSIGN_INTEGRATION_KEY"), // integration key / client_id + "sub": utils.GetProperty("DOCUSIGN_INTEGRATION_USER_ID"), // user_id, in PROD should be the EasyCLA Admin user account + "aud": utils.GetProperty("DOCUSIGN_AUTH_SERVER"), // account.docusign.com or account-d.docusign.com (for dev) + "iat": time.Now().Unix(), + "exp": time.Now().Add(time.Hour).Unix(), // one hour appears to be the max, minus 60 seconds + "scope": "signature impersonation", + } + // log.WithFields(f).Debugf("claims: %+v", claims) + + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + + // DEBUG - remove + // log.WithFields(f).Debugf("integration key (iss) : %s", utils.GetProperty("DOCUSIGN_INTEGRATION_KEY")) + // log.WithFields(f).Debugf("integration user (sub) : %s", utils.GetProperty("DOCUSIGN_INTEGRATION_USER_ID")) + // log.WithFields(f).Debugf("integration host : %s", getDocuSignAccountHost()) + + token.Header["alg"] = "RS256" + token.Header["typ"] = "JWT" + + //publicKey, publicKeyErr := jwt.ParseRSAPublicKeyFromPEM([]byte(utils.GetProperty("DOCUSIGN_RSA_PUBLIC_KEY"))) + //if publicKeyErr != nil { + // log.WithFields(f).WithError(publicKeyErr).Warnf("problem decoding docusign public key") + // return "", publicKeyErr + //} + privateKey, privateKeyErr := jwt.ParseRSAPrivateKeyFromPEM([]byte(utils.GetProperty("DOCUSIGN_RSA_PRIVATE_KEY"))) + // privateKey, privateKeyErr := jwt.ParseRSAPrivateKeyFromPEM([]byte(docusignPrivateKey)) + if privateKeyErr != nil { + log.WithFields(f).WithError(privateKeyErr).Warnf("problem decoding docusign private key") + return "", privateKeyErr } -} \ No newline at end of file + // log.WithFields(f).Debugf("private key: %s", utils.GetProperty("DOCUSIGN_RSA_PRIVATE_KEY")) + + signedToken, signedTokenErr := token.SignedString(privateKey) + if signedTokenErr != nil { + log.WithFields(f).WithError(signedTokenErr).Warnf("problem generating the signed token") + } + // log.WithFields(f).Debugf("signed token: %s", signedToken) + + return signedToken, signedTokenErr +} diff --git a/cla-backend-go/v2/sign/models.go b/cla-backend-go/v2/sign/models.go new file mode 100644 index 000000000..a7fc65635 --- /dev/null +++ b/cla-backend-go/v2/sign/models.go @@ -0,0 +1,292 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package sign + +import ( + "database/sql" + "time" +) + +// DocuSignGetTokenRequest is the request body for getting a token from DocuSign +type DocuSignGetTokenRequest struct { + GrantType string `json:"grant_type"` + Assertion string `json:"assertion"` +} + +// DocuSignGetTokenResponse is the response body for getting a token from DocuSign +type DocuSignGetTokenResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + Scope string `json:"scope"` +} + +// DocuSignUserInfoResponse is the response body for getting user info from DocuSign +type DocuSignUserInfoResponse struct { + Sub string `json:"sub"` // holds the GUID API username of the user that is being impersonated + Name string `json:"name"` + GivenName string `json:"given_name"` + FamilyName string `json:"family_name"` + Created string `json:"created"` + Email string `json:"email"` + Accounts []struct { + AccountId string `json:"account_id"` + IsDefault bool `json:"is_default"` + AccountName string `json:"account_name"` + BaseUri string `json:"base_uri"` + } `json:"accounts"` +} + +// DocuSignEnvelopeRequest is the request body for an envelope from DocuSign, see: https://developers.docusign.com/docs/esign-rest-api/reference/envelopes/envelopes/create/ +type DocuSignEnvelopeRequest struct { + EnvelopeId string `json:"envelopeId,omitempty"` // The envelope ID of the envelope + EnvelopeIdStamping string `json:"envelopeIdStamping,omitempty"` // When true, Envelope ID Stamping is enabled. After a document or attachment is stamped with an Envelope ID, the ID is seen by all recipients and becomes a permanent part of the document and cannot be removed. + TemplateId string `json:"templateId,omitempty"` // The ID of the template. If a value is not provided, DocuSign generates a value. + Documents []DocuSignDocument `json:"document,omitempty"` // A data model containing details about the documents associated with the envelope + DocumentBase64 string `json:"documentBase64,omitempty"` // The document's bytes. This field can be used to include a base64 version of the document bytes within an envelope definition instead of sending the document using a multi-part HTTP request. The maximum document size is smaller if this field is used due to the overhead of the base64 encoding. + DocumentsCombinedUri string `json:"documentsCombinedUri,omitempty"` // The URI for retrieving all of the documents associated with the envelope as a single PDF file. + DocumentsUri string `json:"documentsUri,omitempty"` // The URI for retrieving all of the documents associated with the envelope as separate files. + EmailSubject string `json:"emailSubject,omitempty"` // EmailSubject - The subject line of the email message that is sent to all recipients. + EmailBlurb string `json:"emailBlurb,omitempty"` // EmailBlurb - This is the same as the email body. If specified it is included in email body for all envelope recipients. + Recipients DocuSignRecipientType `json:"recipients,omitempty"` + TemplateRoles []DocuSignTemplateRole `json:"templateRoles,omitempty"` + + /* Status + Indicates the envelope status. Valid values when creating an envelope are: + + created: The envelope is created as a draft. It can be modified and sent later. + sent: The envelope will be sent to the recipients after the envelope is created. + + You can query these additional statuses once the recipients have interacted with the envelope. + + completed: The recipients have finished working with the envelope: the documents are signed and all required tabs are filled in. + declined: The envelope has been declined by the recipients. + delivered: The envelope has been delivered to the recipients. + signed: The envelope has been signed by the recipients. + voided: The envelope is no longer valid and recipients cannot access or sign the envelope. + + */ + Status string `json:"status,omitempty"` +} + +// DocuSignDocument is the data model for a document from DocuSign +type DocuSignDocument struct { + DocumentId string `json:"documentId,omitempty"` // Specifies the document ID of this document. This value is used by tabs to determine which document they appear in. + DocumentBase64 string `json:"documentBase64,omitempty"` // The document's bytes. This field can be used to include a base64 version of the document bytes within an envelope definition instead of sending the document using a multi-part HTTP request. The maximum document size is smaller if this field is used due to the overhead of the base64 encoding.0:w + FileExtension string `json:"fileExtension,omitempty"` // The file extension type of the document. Non-PDF documents are converted to PDF. If the document is not a PDF, fileExtension is required. If you try to upload a non-PDF document without a fileExtension, you will receive an "unable to load document" error message. The file extension type of the document. If the document is not a PDF it is converted to a PDF. + FileFormatHint string `json:"fileFormatHint,omitempty"` + IncludeInDownload string `json:"includeInDownload,omitempty"` // When set to true, the document is included in the combined document download. + Name string `json:"name,omitempty"` // The name of the document. This is the name that appears in the list of documents when managing an envelope. + Order string `json:"order,omitempty"` // The order in which to sort the results. Valid values are: asc, desc +} + +// DocuSignRecipientType is the data model for a recipient from DocuSign +type DocuSignRecipientType struct { + Agents []DocuSignRecipient `json:"agent,omitempty"` + CarbonCopies []DocuSignRecipient `json:"carbonCopy,omitempty"` + CertifiedDeliveries []DocuSignRecipient `json:"certifiedDelivery,omitempty"` + Editors []DocuSignRecipient `json:"editor,omitempty"` + InPersonSigners []DocuSignRecipient `json:"inPersonSigner,omitempty"` + Intermediaries []DocuSignRecipient `json:"intermediary,omitempty"` + Notaries []DocuSignRecipient `json:"notaryRecipient,omitempty"` + Participants []DocuSignRecipient `json:"participant,omitempty"` + Seals []DocuSignRecipient `json:"seals,omitempty"` // A list of electronic seals to apply to documents. + Signers []DocuSignRecipient `json:"signers,omitempty"` // A list of signers on the envelope. + Witnesses []DocuSignRecipient `json:"witness,omitempty"` // A list of signers who act as witnesses for an envelope. + RecipientCount string `json:"recipientCount,omitempty"` // The number of recipients in the envelope. +} + +// DocuSignRecipient is the data model for an editor or signer from DocuSign +type DocuSignRecipient struct { + RecipientId string `json:"recipientId,omitempty"` // Unique for the recipient. It is used by the tab element to indicate which recipient is to sign the document. + + ClientUserId string `json:"clientUserId,omitempty"` // Specifies whether the recipient is embedded or remote. If the clientUserId property is not null then the recipient is embedded. Use this field to associate the signer with their userId in your app. Authenticating the user is the responsibility of your app when you use embedded signing. + + /* The recipient type, as specified by the following values: + agent: Agent recipients can add name and email information for recipients that appear after the agent in routing order. + carbonCopy: Carbon copy recipients get a copy of the envelope but don't need to sign, initial, date, or add information to any of the documents. This type of recipient can be used in any routing order. + certifiedDelivery: Certified delivery recipients must receive the completed documents for the envelope to be completed. They don't need to sign, initial, date, or add information to any of the documents. + editor: Editors have the same management and access rights for the envelope as the sender. Editors can add name and email information, add or change the routing order, set authentication options, and can edit signature/initial tabs and data fields for the remaining recipients. + inPersonSigner: In-person recipients are DocuSign users who act as signing hosts in the same physical location as the signer. + intermediaries: Intermediary recipients can optionally add name and email information for recipients at the same or subsequent level in the routing order. + seal: Electronic seal recipients represent legal entities. + signer: Signers are recipients who must sign, initial, date, or add data to form fields on the documents in the envelope. + witness: Witnesses are recipients whose signatures affirm that the identified signers have signed the documents in the envelope. + */ + RecipientType string `json:"recipientType,omitempty"` + + RoleName string `json:"roleName,omitempty"` // Optional element. Specifies the role name associated with the recipient. This property is required when you are working with template recipients. + + RoutingOrder string `json:"routingOrder,omitempty"` // Specifies the routing order of the recipient in the envelope. + + Name string `json:"name,omitempty"` // The full legal name of the recipient. Maximum Length: 100 characters. Note: You must always set a value for this property in requests, even if firstName and lastName are set. + FirstName string `json:"firstName,omitempty"` // recipient's first name (50 characters maximum) + LastName string `json:"lastName,omitempty"` // recipient's last name + Email string `json:"email,omitempty"` // recipient's email address + Note string `json:"note,omitempty"` // A note sent to the recipient in the signing email. This note is unique to this recipient. In the user interface, it appears near the upper left corner of the document on the signing screen. Maximum Length: 1000 characters. + + Tabs DocuSignTab `json:"tabs"` // The tabs associated with the recipient. The tabs property enables you to programmatically position tabs on the document. For example, you can specify that the SIGN_HERE tab is placed at a given (x,y) location on the document. You can also specify the font, font color, font size, and other properties of the text in the tab. You can also specify the location and size of the tab. For example, you can specify that the tab is 50 pixels wide and 20 pixels high. You can also specify the page number on which the tab is located and whether the tab is located in a document, a template, or an inline template. For more information about tabs, see the Tabs section of the REST API documentation. +} + +// DocuSignTab is the data model for a tab from DocuSign +type DocuSignTab struct { + ApproveTabs []DocuSignTabDetails `json:"approveTabs,omitempty"` + CheckBoxTabs []DocuSignTabDetails `json:"checkboxTabs,omitempty"` + CommentThreadTabs []DocuSignTabDetails `json:"commentThreadTabs,omitempty"` + CommissionCountyTabs []DocuSignTabDetails `json:"commissionCountyTabs,omitempty"` + CommissionExpirationTabs []DocuSignTabDetails `json:"commissionExpirationTabs,omitempty"` + CommissionNumberTabs []DocuSignTabDetails `json:"commissionNumberTabs,omitempty"` + CommissionStateTabs []DocuSignTabDetails `json:"commissionStateTabs,omitempty"` + CompanyTabs []DocuSignTabDetails `json:"companyTabs,omitempty"` + DateSignedTabs []DocuSignTabDetails `json:"dateSignedTabs,omitempty"` + DateTabs []DocuSignTabDetails `json:"dateTabs,omitempty"` + DeclinedTabs []DocuSignTabDetails `json:"declineTabs,omitempty"` + DrawTabs []DocuSignTabDetails `json:"drawTabs,omitempty"` + EmailAddressTabs []DocuSignTabDetails `json:"emailAddressTabs,omitempty"` + EmailTabs []DocuSignTabDetails `json:"emailTabs,omitempty"` + EnvelopeIdTabs []DocuSignTabDetails `json:"envelopeIdTabs,omitempty"` + FirstNameTabs []DocuSignTabDetails `json:"firstNameTabs,omitempty"` + FormulaTabs []DocuSignTabDetails `json:"formulaTab,omitempty"` + FullNameTabs []DocuSignTabDetails `json:"fullNameTabs,omitempty"` + InitialHereTabs []DocuSignTabDetails `json:"initialHereTabs,omitempty"` + LastNameTabs []DocuSignTabDetails `json:"lastNameTabs,omitempty"` + ListTabs []DocuSignTabDetails `json:"listTabs,omitempty"` + NotarizeTabs []DocuSignTabDetails `json:"notarizeTabs,omitempty"` + NotarySealTabs []DocuSignTabDetails `json:"notarySealTabs,omitempty"` + NoteTabs []DocuSignTabDetails `json:"noteTabs,omitempty"` + NumberTabs []DocuSignTabDetails `json:"numberTabs,omitempty"` + NumericalTabs []DocuSignTabDetails `json:"numericalTabs,omitempty"` + PhoneNumberTabs []DocuSignTabDetails `json:"phoneNumberTabs,omitempty"` + PolyLineOverlayTabs []DocuSignTabDetails `json:"polyLineOverlayTabs,omitempty"` + PrefillTabs []DocuSignTabDetails `json:"prefillTabs,omitempty"` + RadioGroupTabs []DocuSignTabDetails `json:"radioGroupTabs,omitempty"` + SignerAttachmentTabs []DocuSignTabDetails `json:"signerAttachmentTabs,omitempty"` + SignHereTabs []DocuSignTabDetails `json:"signHereTabs,omitempty"` + SmartSectionTabs []DocuSignTabDetails `json:"smartSectionTabs,omitempty"` + SSNTabs []DocuSignTabDetails `json:"ssnTabs,omitempty"` + TabGroups []DocuSignTabDetails `json:"tabGroupTabs,omitempty"` + TextTabs []DocuSignTabDetails `json:"textTabs,omitempty"` + TitleTabs []DocuSignTabDetails `json:"titleTabs,omitempty"` + ViewTabs []DocuSignTabDetails `json:"viewTabs,omitempty"` + ZipTabs []DocuSignTabDetails `json:"zipTabs,omitempty"` +} + +// DocuSignTabDetails is the data model for a tab from DocuSign +type DocuSignTabDetails struct { + AnchorCaseSensitive string `json:"anchorCaseSensitive,omitempty"` // anchor case sensitive flag, "true" or "false" + AnchorIgnoreIfNotPresent string `json:"anchorIgnoreIfNotPresent,omitempty"` // When true, this tab is ignored if the anchorString is not found in the document. + AnchorHorizontalAlignment string `json:"anchorHorizontalAlignment,omitempty"` // This property controls how anchor tabs are aligned in relation to the anchor text. Possible values are : left: Aligns the left side of the tab with the beginning of the first character of the matching anchor word. This is the default value. right: Aligns the tab’s left side with the last character of the matching anchor word. + AnchorMatchWholeWord string `json:"anchorMatchWholeWord,omitempty"` // When true, the text string in a document must match the value of the anchorString property in its entirety for an anchor tab to be created. The default value is false. For example, when set to true, if the input is man then man will match but manpower, fireman, and penmanship will not. When false, if the input is man then man, manpower, fireman, and penmanship will all match. + AnchorString string `json:"anchorString,omitempty"` // Specifies the string to find in the document and use as the basis for tab placement + AnchorUnits string `json:"anchorUnits,omitempty"` // anchor units, pixels, cms, mms + AnchorXOffset string `json:"anchorXOffset,omitempty"` // anchor x offset + AnchorYOffset string `json:"anchorYOffset,omitempty"` // anchor y offset + Bold string `json:"bold,omitempty"` // bold flag, "true" or "false" + DocumentId string `json:"documentId,omitempty"` // Specifies the document ID number that the tab is placed on. This must refer to an existing Document's ID attribute. + Font string `json:"font,omitempty"` // font + FontSize string `json:"fontSize,omitempty"` // font size + Height string `json:"height,omitempty"` // The height of the tab in pixels. Must be an integer. + Locked string `json:"locked,omitempty"` // locked flag, "true" or "false" + MinNumericalValue string `json:"minNumericalValue,omitempty"` // minimum numerical value, such as "0", used for validation of numerical tabs + MaxNumericalValue string `json:"maxNumericalValue,omitempty"` // maximum numerical value, such as "100", used for validation of numerical tabs + Name string `json:"name,omitempty"` // The name of the tab. For example, Sign Here or Initial Here. If the tooltip attribute is not set, this value will be displayed as the custom tooltip text. + Optional string `json:"optional,omitempty"` // When true, the recipient does not need to complete this tab to complete the signing process + PageNumber string `json:"pageNumber,omitempty"` // Specifies the page number on which the tab is located. Must be 1 for supplemental documents. + Required string `json:"required,omitempty"` // When true, the signer is required to fill out this tab + TabId string `json:"tabId,omitempty"` // tab idj + TabLabel string `json:"tabLabel,omitempty"` // label + TabOrder string `json:"tabOrder,omitempty"` // A positive integer that sets the order the tab is navigated to during signing. Tabs on a page are navigated to in ascending order, starting with the lowest number and moving to the highest. If two or more tabs have the same tabOrder value, the normal auto-navigation setting behavior for the envelope is used. + TabType string `json:"tabType,omitempty"` // Indicates type of tab (for example: signHere or initialHere) + ToolTip string `json:"toolTip,omitempty"` // The text of a tooltip that appears when a user hovers over a form field or tab. + Width string `json:"width,omitempty"` // The width of the tab in pixels. Must be an integer. This is not applicable to Sign Here tab. + XPosition string `json:"xPosition,omitempty"` // x position + YPosition string `json:"yPosition,omitempty"` // x position + ValidationType string `json:"validationType,omitempty"` // validation type, "string", "number", "date", "zipcode", "currency" + Value string `json:"value,omitempty"` +} + +// DocuSignTemplateRole is the request body for a template role from DocuSign +type DocuSignTemplateRole struct { + Name string `json:"name,omitempty"` // the recipient's email address + Email string `json:"email,omitempty"` // the recipient's name + RoleName string `json:"roleName,omitempty"` // the template role name associated with the recipient + ClientUserID string `json:"clientUserId,omitempty"` // Specifies whether the recipient is embedded or remote. If the clientUserId property is not null then the recipient is embedded. Use this field to associate the signer with their userId in your app. Authenticating the user is the responsibility of your app when you use embedded signing. If the clientUserId property is set and either SignerMustHaveAccount or SignerMustLoginToSign property of the account settings is set to true, an error is generated on sending. + RoutingOrder string `json:"routingOrder,omitempty"` // Specifies the routing order of the recipient in the envelope. +} + +// DocuSignEnvelopeResponse is the response body for an envelope from DocuSign, see: https://developers.docusign.com/docs/esign-rest-api/reference/envelopes/envelopes/update/ +type DocuSignEnvelopeResponse struct { + EnvelopeId string `json:"envelopeId,omitempty"` + ErrorDetails struct { + ErrorCode string `json:"errorCode,omitempty"` + Message string `json:"message,omitempty"` + } `json:"errorDetails,omitempty"` +} + +// DocuSignEnvelopeResponseModel is the envelope response model +type DocuSignEnvelopeResponseModel struct { + /* + // Response from: https://developers.docusign.com/docs/esign-rest-api/reference/envelopes/envelopes/get/ + { + "allowMarkup": "false", + "autoNavigation": "true", + "brandId": "56502fe1-xxxx-xxxx-xxxx-97cb5c43176a", + "certificateUri": "/envelopes/4b728be4-xxxx-xxxx-xxxx-d63e23f822b6/documents/certificate", + "createdDateTime": "2016-10-05T01:04:58.1830000Z", + "customFieldsUri": "/envelopes/4b728be4-xxxx-xxxx-xxxx-d63e23f822b6/custom_fields", + "documentsCombinedUri": "/envelopes/4b728be4-xxxx-xxxx-xxxx-d63e23f822b6/documents/combined", + "documentsUri": "/envelopes/4b728be4-xxxx-xxxx-xxxx-d63e23f822b6/documents", + "emailSubject": "Please sign the NDA", + "enableWetSign": "true", + "envelopeId": "4b728be4-xxxx-xxxx-xxxx-d63e23f822b6", + "envelopeIdStamping": "true", + "envelopeUri": "/envelopes/4b728be4-xxxx-xxxx-xxxx-d63e23f822b6", + "initialSentDateTime": "2016-10-05T01:04:58.7770000Z", + "is21CFRPart11": "false", + "isSignatureProviderEnvelope": "false", + "lastModifiedDateTime": "2016-10-05T01:04:58.1830000Z", + "notificationUri": "/envelopes/4b728be4-xxxx-xxxx-xxxx-d63e23f822b6/notification", + "purgeState": "unpurged", + "recipientsUri": "/envelopes/4b728be4-xxxx-xxxx-xxxx-d63e23f822b6/recipients", + "sentDateTime": "2016-10-05T01:04:58.7770000Z", + "status": "sent", + "statusChangedDateTime": "2016-10-05T01:04:58.7770000Z", + "templatesUri": "/envelopes/4b728be4-xxxx-xxxx-xxxx-d63e23f822b6/templates" + } + */ + AllowMarkup string `json:"allowMarkup,omitempty"` + AutoNavigation string `json:"autoNavigation,omitempty"` + BrandId string `json:"brandId,omitempty"` + CertificateUri string `json:"certificateUri,omitempty"` + CreatedDateTime string `json:"createdDateTime,omitempty"` + CustomFieldsUri string `json:"customFieldsUri,omitempty"` + DocumentsCombinedUri string `json:"documentsCombinedUri,omitempty"` + DocumentsUri string `json:"documentsUri,omitempty"` + EmailSubject string `json:"emailSubject,omitempty"` + EnableWetSign string `json:"enableWetSign,omitempty"` + EnvelopeId string `json:"envelopeId,omitempty"` + EnvelopeIdStamping string `json:"envelopeIdStamping,omitempty"` + EnvelopeUri string `json:"envelopeUri,omitempty"` + InitialSentDateTime string `json:"initialSentDateTime,omitempty"` + Is21CFRPart11 string `json:"is21CFRPart11,omitempty"` + IsSignatureProviderEnvelope string `json:"isSignatureProviderEnvelope,omitempty"` + LastModifiedDateTime string `json:"lastModifiedDateTime,omitempty"` + NotificationUri string `json:"notificationUri,omitempty"` + PurgeState string `json:"purgeState,omitempty"` + RecipientsUri string `json:"recipientsUri,omitempty"` + SentDateTime string `json:"sentDateTime,omitempty"` + Status string `json:"status,omitempty"` + StatusChangedDateTime string `json:"statusChangedDateTime,omitempty"` + TemplatesUri string `json:"templatesUri,omitempty"` +} + +// IndividualMembershipDocuSignDBSummaryModel is the data model for an individual membership DocuSign database summary models +type IndividualMembershipDocuSignDBSummaryModel struct { + DocuSignEnvelopeID string `db:"docusign_envelope_id"` + DocuSignEnvelopeCreatedAt string `db:"docusign_envelope_created_at"` + DocuSignEnvelopeSigningStatus string `db:"docusign_envelope_signing_status"` + DocuSignEnvelopeSigningUpdatedAt time.Time `db:"docusign_envelope_signing_updated_at"` + Memo sql.NullString `db:"memo"` + //DocuSignEnvelopeSignedDate time.Time `json:"docusign_envelope_signed_date"` +} diff --git a/cla-backend-go/v2/sign/service.go b/cla-backend-go/v2/sign/service.go index 99b282687..159035e49 100644 --- a/cla-backend-go/v2/sign/service.go +++ b/cla-backend-go/v2/sign/service.go @@ -33,7 +33,6 @@ import ( v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" "github.com/communitybridge/easycla/cla-backend-go/utils" - "github.com/communitybridge/easycla/cla-backend-go/v2/docusign_auth" ) var ( @@ -62,7 +61,7 @@ type ProjectRepo interface { // Service interface defines the sign service methods type Service interface { RequestCorporateSignature(ctx context.Context, lfUsername string, authorizationHeader string, input *models.CorporateSignatureInput) (*models.CorporateSignatureOutput, error) - RequestIndividualSignature(ctx context.Context, input *models.IclaSignatureInput) (*models.IndividualSignatureOutput, error) + RequestIndividualSignature(ctx context.Context, input *models.IndividualSignatureInput) (*models.IndividualSignatureOutput, error) } // service @@ -311,25 +310,24 @@ func (s *service) RequestCorporateSignature(ctx context.Context, lfUsername stri return out.toModel(), nil } -func (s *service) RequestIndividualSignature(ctx context.Context, input *models.IclaSignatureInput) (*models.IndividualSignatureOutput, error) { +func (s *service) RequestIndividualSignature(ctx context.Context, input *models.IndividualSignatureInput) (*models.IndividualSignatureOutput, error) { f := logrus.Fields{ - "functionName": "sign.RequestIndividualSignature", - "authorityEmail": input.AuthorityEmail, - "authorityName": input.AuthorityName, - "companySFID": input.CompanySfid, - "projectSFID": input.ProjectSfid, + "functionName": "sign.RequestIndividualSignature", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectID": input.ProjectID, + "returnURL": input.ReturnURL, + "returnURLType": input.ReturnURLType, + "userID": input.UserID, } log.WithFields(f).Debug("Get Access Token for DocuSign") - accessToken, err := docusignauth.GetAccessToken(integrationKey, userGUID, privateKey) + accessToken, err := s.getAccessToken(ctx) if err != nil { - log.WithFields(f).WithError(err).Warn("unable to get access token for DocuSign") + log.WithFields(f).WithError(err).Warn("unable to get access token") return nil, err } - log.WithFields(f).Debugf("access token: %s", accessToken) return nil, nil - } func requestCorporateSignature(authToken string, apiURL string, input *requestCorporateSignatureInput) (*requestCorporateSignatureOutput, error) {