diff --git a/cla-backend-go/go.mod b/cla-backend-go/go.mod index 2a213510b..ad18f3b97 100644 --- a/cla-backend-go/go.mod +++ b/cla-backend-go/go.mod @@ -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 diff --git a/cla-backend-go/go.sum b/cla-backend-go/go.sum index ad5c78d64..4557d1598 100644 --- a/cla-backend-go/go.sum +++ b/cla-backend-go/go.sum @@ -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= diff --git a/cla-backend-go/serverless.yml b/cla-backend-go/serverless.yml index 8f96b02f9..0f5eb3f50 100644 --- a/cla-backend-go/serverless.yml +++ b/cla-backend-go/serverless.yml @@ -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}} diff --git a/cla-backend-go/swagger/cla.v2.yaml b/cla-backend-go/swagger/cla.v2.yaml index 43397ebbb..64eb14c05 100644 --- a/cla-backend-go/swagger/cla.v2.yaml +++ b/cla-backend-go/swagger/cla.v2.yaml @@ -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 @@ -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: @@ -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: diff --git a/cla-backend-go/v2/docusign_auth/auth.go b/cla-backend-go/v2/docusign_auth/auth.go new file mode 100644 index 000000000..96b137cb4 --- /dev/null +++ b/cla-backend-go/v2/docusign_auth/auth.go @@ -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 +} diff --git a/cla-backend-go/v2/sign/handlers.go b/cla-backend-go/v2/sign/handlers.go index 4d578f994..d3c743acc 100644 --- a/cla-backend-go/v2/sign/handlers.go +++ b/cla-backend-go/v2/sign/handlers.go @@ -4,6 +4,7 @@ package sign import ( + "context" "errors" "fmt" "strings" @@ -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 @@ -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 { diff --git a/cla-backend-go/v2/sign/service.go b/cla-backend-go/v2/sign/service.go index 907eec7e7..99b282687 100644 --- a/cla-backend-go/v2/sign/service.go +++ b/cla-backend-go/v2/sign/service.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "net/http" + "os" "strings" "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" @@ -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 @@ -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 @@ -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",