Skip to content

Commit

Permalink
Feature/Docusign support
Browse files Browse the repository at this point in the history
- Added golang feature for enabling individual and ccla sign process

Signed-off-by: Harold Wanyama <hwanyama@contractor.linuxfoundation.org>
  • Loading branch information
nickmango committed Sep 11, 2023
1 parent acf1767 commit e1c0394
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 0 deletions.
1 change: 1 addition & 0 deletions cla-backend-go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ require (
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310 // indirect
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
github.com/cloudflare/circl v1.3.2 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
Expand Down
2 changes: 2 additions & 0 deletions cla-backend-go/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185/go.mod h1:cFRxtTwTOJkz2x3rQUNCYKWC93yP1VKjR8NUhqFxZNU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
Expand Down
3 changes: 3 additions & 0 deletions cla-backend-go/serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,9 @@ provider:
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}}
DOCUSIGN_AUTH_SERVER: ${file(./env.json):docusign-auth-server, ssm:/cla-docusign-auth-server-${opt:stage}}
DOCUSIGN_PRIVATE_KEY: ${file(./env.json):docusign-auth-server, ssm:/cla-docusign-private-key-${opt:stage}}
DOCUSIGN_USER_ID: ${file(./env.json):docusign-auth-server, ssm:/cla-docusign-user-id-${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}}
CLA_CONTRIBUTOR_V2_BASE: ${file(./env.json):cla-contributor-v2-base, ssm:/cla-contributor-v2-base-${opt:stage}}
Expand Down
74 changes: 74 additions & 0 deletions cla-backend-go/swagger/cla.v2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4125,6 +4125,36 @@ paths:
tags:
- gitlab-sign


/request-individual-signature:
post:
summary: Request for icla sign
description: Initiate the icla signing with docusign
security: []
operationId: requestIndividualSignature
parameters:
- $ref: "#/parameters/x-request-id"
- name: input
in: body
schema:
$ref: '#/definitions/icla-signature-input'
required: true
responses:
'200':
description: 'Success'
headers:
x-request-id:
type: string
description: The unique request ID value - assigned/set by the API Gateway based on the session
schema:
$ref: '#/definitions/individual-signature-output'
'400':
$ref: '#/responses/invalid-request'
'500':
$ref: '#/responses/internal-server-error'
tags:
- sign

responses:
unauthorized:
description: Unauthorized
Expand Down Expand Up @@ -5497,6 +5527,40 @@ definitions:
corporate-contributor:
$ref: './common/corporate-contributor.yaml'

icla-signature-input:
type: object
required:
- project_sfid
- company_sfid
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:
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
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


corporate-signature-input:
type: object
required:
Expand Down Expand Up @@ -5544,6 +5608,16 @@ definitions:
type: string
description: signing url

individual-signature-output:
type: object
properties:
signature_id:
type: string
description: id of the signature
sign_url:
type: string
description: signing url

signed_document:
type: object
properties:
Expand Down
90 changes: 90 additions & 0 deletions cla-backend-go/v2/docusign_auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright The Linux Foundation and each contributor to CommunityBridge.
// SPDX-License-Identifier: MIT

package docusignauth

import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"

jwt "github.com/dgrijalva/jwt-go"
)

var (
baseURL = os.Getenv("DOCUSIGN_BASE_URL")
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) {
// Generate the JWT token
tokenString, err := generateJWT(integrationKey, userGUID, privateKey)
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
}
24 changes: 24 additions & 0 deletions cla-backend-go/v2/sign/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package sign

import (
"context"
"errors"
"fmt"
"strings"
Expand All @@ -20,6 +21,7 @@ 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
Expand Down Expand Up @@ -75,6 +77,28 @@ func Configure(api *operations.EasyclaAPI, service Service) {
}
return sign.NewRequestCorporateSignatureOK().WithPayload(resp)
})

api.SignRequestIndividualSignatureHandler = sign.RequestIndividualSignatureHandlerFunc(
func(params sign.RequestIndividualSignatureParams) middleware.Responder {
reqId := utils.GetRequestID(params.XREQUESTID)
ctx := context.WithValue(params.HTTPRequest.Context(), utils.XREQUESTID, reqId)
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,
}
log.WithFields(f).Debug("processing request")
resp, err := service.RequestIndividualSignature(ctx, params.Input)
if err != nil {
log.WithFields(f).WithError(err).Warn("problem requesting individual signature")
return sign.NewRequestIndividualSignatureBadRequest().WithPayload(errorResponse(reqId, err))
}
return sign.NewRequestIndividualSignatureOK().WithPayload(resp)
})

}

type codedResponse interface {
Expand Down
30 changes: 30 additions & 0 deletions cla-backend-go/v2/sign/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"fmt"
"io"
"net/http"
"os"
"strings"

"github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups"
Expand All @@ -32,6 +33,13 @@ 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 (
integrationKey = os.Getenv("DOCUSIGN_INTEGRATOR_KEY")
userGUID = os.Getenv("DOCUSIGN_USER_ID")
privateKey = os.Getenv("DOCUSIGN_PRIVATE_KEY")
)

// constants
Expand All @@ -54,6 +62,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)
}

// service
Expand Down Expand Up @@ -302,6 +311,27 @@ 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) {
f := logrus.Fields{
"functionName": "sign.RequestIndividualSignature",
"authorityEmail": input.AuthorityEmail,
"authorityName": input.AuthorityName,
"companySFID": input.CompanySfid,
"projectSFID": input.ProjectSfid,
}

log.WithFields(f).Debug("Get Access Token for DocuSign")
accessToken, err := docusignauth.GetAccessToken(integrationKey, userGUID, privateKey)
if err != nil {
log.WithFields(f).WithError(err).Warn("unable to get access token for DocuSign")
return nil, err
}

log.WithFields(f).Debugf("access token: %s", accessToken)
return nil, nil

}

func requestCorporateSignature(authToken string, apiURL string, input *requestCorporateSignatureInput) (*requestCorporateSignatureOutput, error) {
f := logrus.Fields{
"functionName": "requestCorporateSignature",
Expand Down

0 comments on commit e1c0394

Please sign in to comment.