diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 3e4d47925..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,1183 +0,0 @@ -# Copyright The Linux Foundation and each contributor to CommunityBridge. -# SPDX-License-Identifier: MIT -version: 2.1 - -environment: - BASH_ENV: ~/.bashrc - -setup_aws: &setup_aws - run: - name: Setup AWS key and Profile - command: | - touch ${BASH_ENV} - if ! grep -q AWS_ACCESS_KEY_ID ${BASH_ENV} ; then - echo "export AWS_ACCESS_KEY_ID='${!AWS_ACCESS_KEY_ID_ENV_VAR}'" >> ${BASH_ENV} - echo "Added AWS_ACCESS_KEY_ID to ${BASH_ENV}" - else - echo "Skipped adding AWS_ACCESS_KEY_ID to ${BASH_ENV} - already there" - fi - - if ! grep -q AWS_SECRET_ACCESS_KEY ${BASH_ENV} ; then - echo "export AWS_SECRET_ACCESS_KEY='${!AWS_SECRET_ACCESS_KEY_ENV_VAR}'" >> ${BASH_ENV} - echo "Added AWS_SECRET_ACCESS_KEY to ${BASH_ENV}" - else - echo "Skipped adding AWS_SECRET_ACCESS_KEY to ${BASH_ENV} - already there" - fi - - echo "Installing Profile '${AWS_PROFILE}'..." - mkdir -p ~/.aws - - touch ~/.aws/config - if ! grep -q AWS_PROFILE ~/.aws/config; then - printf "[profile ${AWS_PROFILE}]\nregion=${AWS_REGION}\noutput=json" > ~/.aws/config - echo "Added ${AWS_PROFILE} profile to ~/.aws/config" - else - echo "Skipped adding ${AWS_PROFILE} to ~/.aws/config - already there" - fi - - touch ~/.aws/credentials - if ! grep -q AWS_PROFILE ~/.aws/credentials; then - printf "[${AWS_PROFILE}]\naws_access_key_id=${!AWS_ACCESS_KEY_ID_ENV_VAR}\naws_secret_access_key=${!AWS_SECRET_ACCESS_KEY_ENV_VAR}" > ~/.aws/credentials - echo "Added ${AWS_PROFILE} profile to ~/.aws/credentials" - else - echo "Skipped adding ${AWS_PROFILE} to ~/.aws/credentials - already there" - fi - - if ! grep -q AWS_PROFILE ${BASH_ENV}; then - echo "export AWS_PROFILE=${AWS_PROFILE}" >> ${BASH_ENV} - echo "Added ${AWS_PROFILE} profile to ${BASH_ENV}" - else - echo "Skipped adding ${AWS_PROFILE} to ${BASH_ENV} - already there" - fi - -install_aws_cli: &install_aws_cli - run: - name: Install AWS CLI Tools - command: | - sudo apt-get update - sudo apt-get install -y awscli - -set_functional_test_environment: &set_functional_test_environment - run: - name: set deployment environment - command: | - cd && echo "Setting environment in $BASH_ENV for stage ${STAGE}" && touch $BASH_ENV - - # Note, we place single quotes around the values to ensure any values - # with dollar signs are not intrepreted and expanded by accident - # Default Test User (functional test) - echo "export AUTH0_USERNAME='${!AUTH0_USERNAME_ENV_VAR}'" >> ${BASH_ENV} - echo "export AUTH0_PASSWORD='${!AUTH0_PASSWORD_ENV_VAR}'" >> ${BASH_ENV} - echo "export AUTH0_CLIENT_ID='${!AUTH0_CLIENT_ID_ENV_VAR}'" >> ${BASH_ENV} - - # Prospective CLA Manager User (for functional tests) - echo "export AUTH0_USER1_EMAIL='${!AUTH0_USER1_EMAIL_ENV_VAR}'" >> ${BASH_ENV} - echo "export AUTH0_USER1_USERNAME='${!AUTH0_USER1_USERNAME_ENV_VAR}'" >> ${BASH_ENV} - echo "export AUTH0_USER1_PASSWORD='${!AUTH0_USER1_PASSWORD_ENV_VAR}'" >> ${BASH_ENV} - echo "export AUTH0_USER1_CLIENT_ID='${!AUTH0_USER1_CLIENT_ID_ENV_VAR}'" >> ${BASH_ENV} - - # CLA Manager User (for functional tests) - echo "export AUTH0_USER2_EMAIL='${!AUTH0_USER2_EMAIL_ENV_VAR}'" >> ${BASH_ENV} - echo "export AUTH0_USER2_USERNAME='${!AUTH0_USER2_USERNAME_ENV_VAR}'" >> ${BASH_ENV} - echo "export AUTH0_USER2_PASSWORD='${!AUTH0_USER2_PASSWORD_ENV_VAR}'" >> ${BASH_ENV} - echo "export AUTH0_USER2_CLIENT_ID='${!AUTH0_USER2_CLIENT_ID_ENV_VAR}'" >> ${BASH_ENV} - - # CLA Manager Intel (for functional tests) - echo "export AUTH0_USER3_EMAIL='${!AUTH0_USER3_EMAIL_ENV_VAR}'" >> ${BASH_ENV} - echo "export AUTH0_USER3_USERNAME='${!AUTH0_USER3_USERNAME_ENV_VAR}'" >> ${BASH_ENV} - echo "export AUTH0_USER3_PASSWORD='${!AUTH0_USER3_PASSWORD_ENV_VAR}'" >> ${BASH_ENV} - echo "export AUTH0_USER3_CLIENT_ID='${!AUTH0_USER3_CLIENT_ID_ENV_VAR}'" >> ${BASH_ENV} - - # CLA Manager AT&T (for functional tests) - echo "export AUTH0_USER4_EMAIL='${!AUTH0_USER4_EMAIL_ENV_VAR}'" >> ${BASH_ENV} - echo "export AUTH0_USER4_USERNAME='${!AUTH0_USER4_USERNAME_ENV_VAR}'" >> ${BASH_ENV} - echo "export AUTH0_USER4_PASSWORD='${!AUTH0_USER4_PASSWORD_ENV_VAR}'" >> ${BASH_ENV} - echo "export AUTH0_USER4_CLIENT_ID='${!AUTH0_USER4_CLIENT_ID_ENV_VAR}'" >> ${BASH_ENV} - - # Project Manager (for functional tests) - echo "export AUTH0_USER5_EMAIL='${!AUTH0_USER5_EMAIL_ENV_VAR}'" >> ${BASH_ENV} - echo "export AUTH0_USER5_USERNAME='${!AUTH0_USER5_USERNAME_ENV_VAR}'" >> ${BASH_ENV} - echo "export AUTH0_USER5_PASSWORD='${!AUTH0_USER5_PASSWORD_ENV_VAR}'" >> ${BASH_ENV} - echo "export AUTH0_USER5_CLIENT_ID='${!AUTH0_USER5_CLIENT_ID_ENV_VAR}'" >> ${BASH_ENV} - -step-library: - - &install-node-8 - run: - name: Install node 8 - command: | - set +e - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.2/install.sh | bash - [ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh" - node_version="v8.17.0" - echo "Installing node ${node_version}..." - nvm install ${node_version} - nvm alias default ${node_version} - echo "[ -s \"${NVM_DIR}/nvm.sh\" ] && . \"${NVM_DIR}/nvm.sh\"" >> $BASH_ENV - - - &install-node-12 - run: - name: Install node 12 - command: | - set +e - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.2/install.sh | bash - [ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh" - node_version="v12.20.0" - echo "Installing node ${node_version}..." - nvm install ${node_version} - nvm alias default ${node_version} - echo "[ -s \"${NVM_DIR}/nvm.sh\" ] && . \"${NVM_DIR}/nvm.sh\"" >> $BASH_ENV - -jobs: - # Builds - buildBackend: &buildBackendAnchor - docker: - - image: circleci/python:3.7.9-node - steps: - - checkout - - add_ssh_keys: - fingerprints: - - "e9:13:85:f1:b1:a1:25:bf:f5:44:34:66:82:1e:31:59" - - *setup_aws - - run: echo 'export NVM_DIR=${HOME}/.nvm' >> $BASH_ENV - - *install-node-12 - - run: - name: Install Top Level Dependencies - command: | - echo "Node version is: $(node --version)" - echo "Running top level install..." - yarn install - - *install_aws_cli - - run: - name: Setup Backend - command: | - cd cla-backend - yarn install - echo "Upgrading pip..." - python3 -m pip install --upgrade pip - sudo pip install -r requirements.txt - - run: - name: lint - command: | - cd cla-backend - ./check-headers.sh - # Lint will always pass for now - need to continue addressing lint issues - pylint cla/*.py || true - - run: - name: test - command: | - cd cla-backend - export GITHUB_OAUTH_TOKEN=${GITHUB_OAUTH_TOKEN} - pytest "cla/tests" -p no:warnings --cov="cla" - - buildBackendDev: - <<: *buildBackendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_DEV - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_DEV - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: dev - - buildBackendStaging: - <<: *buildBackendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_STAGING - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_STAGING - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: staging - - buildBackendProd: - <<: *buildBackendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_PROD - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_PROD - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: prod - - buildGoBackend: &buildGoBackendAnchor - docker: - - image: circleci/golang:1.15.6 - working_directory: /go/src/github.com/communitybridge/easycla/ - steps: - - checkout - - add_ssh_keys: - fingerprints: - - "e9:13:85:f1:b1:a1:25:bf:f5:44:34:66:82:1e:31:59" - - run: echo 'export NVM_DIR=${HOME}/.nvm' >> $BASH_ENV - - *install-node-12 - - run: echo 'export GO111MODULE=on' >> $BASH_ENV - - run: - name: Setup - command: | - source ${BASH_ENV} - echo "Installing python..." - sudo apt-get update - sudo apt install -y software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa -y - sudo apt-get install -y python3.7 python3-pip - echo "Upgrading pip..." - python3 -m pip install --upgrade pip - echo "Python 3 version:" - python3 --version - make -f cla-backend-go/Makefile setup-dev - - run: - name: Clean - command: | - cd cla-backend-go - make clean - - run: - name: Dependencies - command: | - cd cla-backend-go - make deps - - run: - name: Build Swagger - command: | - cd cla-backend-go - make swagger - - run: - name: Build - command: | - cd cla-backend-go - echo "Building AWS Lambda - API..." - make build-aws-lambda-linux - echo "Building AWS Metrics Lambda..." - make build-metrics-lambda-linux - echo "Building AWS Metrics Report Lambda..." - make build-metrics-report-lambda - echo "Building AWS Lambda - DynamoDB Events Handler..." - make build-dynamo-events-lambda-linux - echo "Building AWS Lambda - Zip Builder Scheduler..." - make build-zipbuilder-scheduler-lambda-linux - echo "Building AWS Lambda - Zip Builder Handler..." - make build-zipbuilder-lambda-linux - echo "Building Functional Tests..." - make build-functional-tests-linux - echo "Building User Subscribe..." - make build-user-subscribe-lambda-linux - - run: - name: Test - command: | - cd cla-backend-go - make test - - run: - name: Lint - command: | - cd cla-backend-go - make lint - - run: - name: Move Binary - command: | - mv cla-backend-go ~/cla-backend-go - - persist_to_workspace: - root: ~/ - paths: - - cla-backend-go/backend-aws-lambda - - cla-backend-go/user-subscribe-lambda - - cla-backend-go/metrics-aws-lambda - - cla-backend-go/metrics-report-lambda - - cla-backend-go/dynamo-events-lambda - - cla-backend-go/zipbuilder-scheduler-lambda - - cla-backend-go/zipbuilder-lambda - - cla-backend-go/functional-tests - - buildGoBackendDev: - <<: *buildGoBackendAnchor - environment: - STAGE: dev - - buildGoBackendStaging: - <<: *buildGoBackendAnchor - environment: - STAGE: staging - - buildGoBackendProd: - <<: *buildGoBackendAnchor - environment: - STAGE: prod - - # Deploys - deployBackend: &deployBackendAnchor - docker: - - image: circleci/python:3.7.9-node - steps: - - attach_workspace: - at: ~/ - - checkout - - add_ssh_keys: - fingerprints: - - "e9:13:85:f1:b1:a1:25:bf:f5:44:34:66:82:1e:31:59" - - *setup_aws - - run: echo 'export NVM_DIR=${HOME}/.nvm' >> $BASH_ENV - - *install-node-12 - - run: - name: Install Top Level Dependencies - command: | - echo "Node version is: $(node --version)" - echo "Running top level install..." - yarn install - - run: - name: Deploy EasyCLA v1 - command: | - echo "Using AWS profile: ${AWS_PROFILE}" - echo "Stage is: ${STAGE}" - - # -------------------------------------------------------------- - ## Debug to confirm the binary files were restored - echo "Directory: ~/" - ls -alF ~/ - echo "Directory: ~/cla-backend-go/" - ls -alF ~/cla-backend-go/ - ## End Debug - # -------------------------------------------------------------- - - # Copy over the go backend binary to the common cla-backend folder (they share a single serverless.yml config) - cp ~/cla-backend-go/backend-aws-lambda ~/project/cla-backend/ - cp ~/cla-backend-go/user-subscribe-lambda ~/project/cla-backend/ - cp ~/cla-backend-go/metrics-aws-lambda ~/project/cla-backend/ - cp ~/cla-backend-go/metrics-report-lambda ~/project/cla-backend/ - cp ~/cla-backend-go/dynamo-events-lambda ~/project/cla-backend/ - cp ~/cla-backend-go/zipbuilder-scheduler-lambda ~/project/cla-backend/ - cp ~/cla-backend-go/zipbuilder-lambda ~/project/cla-backend/ - - ls -alF ~/project/cla-backend/ - pushd ~/project/cla-backend - echo "Directory: $(pwd)" - yarn install - - if [[ ! -f backend-aws-lambda ]]; then echo "Missing backend-aws-lambda binary file. Exiting..."; exit 1; fi - if [[ ! -f user-subscribe-lambda ]]; then echo "Missing user-subscribe-lambda binary file. Exiting..."; exit 1; fi - if [[ ! -f metrics-aws-lambda ]]; then echo "Missing metrics-aws-lambda binary file. Exiting..."; exit 1; fi - if [[ ! -f metrics-report-lambda ]]; then echo "Missing metrics-report-lambda binary file. Exiting..."; exit 1; fi - if [[ ! -f dynamo-events-lambda ]]; then echo "Missing dynamo-events-lambda binary file. Exiting..."; exit 1; fi - if [[ ! -f zipbuilder-lambda ]]; then echo "Missing zipbuilder-lambda binary file. Exiting..."; exit 1; fi - if [[ ! -f zipbuilder-scheduler-lambda ]]; then echo "Missing zipbuilder-scheduler-lambda binary file. Exiting..."; exit 1; fi - if [[ ! -f serverless.yml ]]; then echo "Missing serverless.yml file. Exiting..."; exit 1; fi - if [[ ! -f serverless-authorizer.yml ]]; then echo "Missing serverless-authorizer.yml file. Exiting..."; exit 1; fi - yarn sls deploy --force --stage ${STAGE} --region us-east-1 - - run: - name: Deploy EasyCLA v2 - command: | - echo "Using AWS profile: ${AWS_PROFILE}" - echo "Stage is: ${STAGE}" - - # -------------------------------------------------------------- - ## Debug to confirm the binary files were restored - echo "Directory: ~/" - ls -alF ~/ - echo "Directory: ~/cla-backend-go/" - ls -alF ~/cla-backend-go/ - ## End Debug - # -------------------------------------------------------------- - - cp ~/cla-backend-go/backend-aws-lambda ~/project/cla-backend-go/ - cp ~/cla-backend-go/user-subscribe-lambda ~/project/cla-backend-go/ - echo "Directory: ~/project/cla-backend-go/" - ls -alF ~/project/cla-backend-go/ - pushd ~/project/cla-backend-go - echo "Directory: $(pwd)" - if [[ ! -f backend-aws-lambda ]]; then echo "Missing backend-aws-lambda binary file. Exiting..."; exit 1; fi - if [[ ! -f user-subscribe-lambda ]]; then echo "Missing user-subscribe-lambda binary file. Exiting..."; exit 1; fi - yarn install - - # Deploy to us-east-2 - if [[ ! -f serverless.yml ]]; then echo "Missing serverless.yml file in $(pwd). Exiting..."; exit 1; fi - yarn sls deploy --force --stage ${STAGE} --region us-east-2 - - run: - name: Service Check - command: | - sudo apt-get install -y curl - v2_url='' - v3_url='' - v4_url='' - if [[ "${STAGE}" == "prod" ]]; then - v2_url=https://api.easycla.lfx.linuxfoundation.org/v2/health - v3_url=https://api.easycla.lfx.linuxfoundation.org/v3/ops/health - v4_url=https://api-gw.platform.linuxfoundation.org/cla-service/v4/ops/health - else - v2_url=https://api.lfcla.${STAGE}.platform.linuxfoundation.org/v2/health - v3_url=https://api.lfcla.${STAGE}.platform.linuxfoundation.org/v3/ops/health - v4_url=https://api-gw.${STAGE}.platform.linuxfoundation.org/cla-service/v4/ops/health - fi - - echo "Validating v2 backend using endpoint: ${v2_url}" - curl --fail -XGET ${v2_url} - exit_code=$? - if [[ ${exit_coe} -eq 0 ]]; then - echo "Successful response from endpoint: ${v2_url}" - else - echo "Failed to get a successful response from endpoint: ${v2_url}" - exit ${exit_code} - fi - - echo "Validating v3 backend using endpoint: ${v3_url}" - curl --fail -XGET ${v3_url} - exit_code=$? - if [[ ${exit_coe} -eq 0 ]]; then - echo "Successful response from endpoint: ${v3_url}" - # JSON response should include "Status": "healthy" - if [[ `curl -s -XGET ${v3_url} | jq -r '.Status'` == "healthy" ]]; then - echo "Service is healthy" - else - echo "Service is NOT healthy" - exit -1 - fi - else - echo "Failed to get a successful response from endpoint: ${v3_url}" - exit ${exit_code} - fi - - echo "Validating v4 backend using endpoint: ${v4_url}" - curl --fail -XGET ${v4_url} - exit_code=$? - if [[ ${exit_coe} -eq 0 ]]; then - echo "Successful response from endpoint: ${v4_url}" - # JSON response should include "Status": "healthy" - if [[ `curl -s -XGET ${v4_url} | jq -r '.Status'` == "healthy" ]]; then - echo "Service is healthy" - else - echo "Service is NOT healthy" - exit -1 - fi - else - echo "Failed to get a successful response from endpoint: ${v4_url}" - exit ${exit_code} - fi - - deployBackendDev: - <<: *deployBackendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_DEV - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_DEV - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: dev - ROOT_DOMAIN: lfcla.dev.platform.linuxfoundation.org - PRODUCT_DOMAIN: dev.lfcla.com - - deployBackendStaging: - <<: *deployBackendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_STAGING - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_STAGING - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: staging - ROOT_DOMAIN: lfcla.staging.platform.linuxfoundation.org - PRODUCT_DOMAIN: staging.lfcla.com - - deployBackendProd: - <<: *deployBackendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_PROD - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_PROD - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: prod - ROOT_DOMAIN: lfcla.platform.linuxfoundation.org - PRODUCT_DOMAIN: lfcla.com - - buildFrontend: &buildFrontendAnchor - docker: - - image: circleci/node:8-browsers - steps: - - checkout - - *setup_aws - - run: echo 'export NVM_DIR=${HOME}/.nvm' >> $BASH_ENV - - *install-node-12 - - run: - name: Install Top Level Dependencies - command: | - echo "Node version is: $(node --version)" - echo "Running top level install..." - yarn install - - *install-node-8 - - run: - name: Install UI Dependencies - command: | - pushd $PROJECT_DIR - echo "Running yarn install in folder: `pwd`. This will run yarn install in several places - see output below." - yarn install-frontend - popd - - run: - name: Build UI Source - command: | - echo "Building src..." - pushd $PROJECT_DIR/src - echo "AWS_PROFILE=${AWS_PROFILE}" - echo "AWS_REGION=${AWS_REGION}" - ls ~/.aws - cat ${BASH_ENV} - yarn prebuild:${STAGE} - yarn build - popd - - run: - name: Build Edge Source - command: | - echo "Building edge..." - pushd $PROJECT_DIR/edge - yarn build - popd - - # Build Project Management Console - buildProjectConsoleDev: - <<: *buildFrontendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_DEV - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_DEV - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: dev - PROJECT_DIR: cla-frontend-project-console - ROOT_DOMAIN: lfcla.dev.platform.linuxfoundation.org - PRODUCT_DOMAIN: dev.lfcla.com - - # Build Corporate Console - buildCorporateConsoleDev: - <<: *buildFrontendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_DEV - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_DEV - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: dev - PROJECT_DIR: cla-frontend-corporate-console - ROOT_DOMAIN: lfcla.dev.platform.linuxfoundation.org - PRODUCT_DOMAIN: dev.lfcla.com - - # Build Contributor Console - buildContributorConsoleDev: - <<: *buildFrontendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_DEV - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_DEV - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: dev - PROJECT_DIR: cla-frontend-contributor-console - ROOT_DOMAIN: lfcla.dev.platform.linuxfoundation.org - PRODUCT_DOMAIN: dev.lfcla.com - - deployFrontend: &deployFrontendAnchor - docker: - - image: circleci/node:8-browsers - steps: - - checkout - - *setup_aws - - run: echo 'export NVM_DIR=${HOME}/.nvm' >> $BASH_ENV - - *install-node-12 - - run: - name: Install Top Level Dependencies - command: | - echo "Node version is: $(node --version)" - echo "Running top level install..." - yarn install - - *install-node-8 - - run: - name: Install UI Dependencies - command: | - pushd $PROJECT_DIR - echo "Running yarn install in folder: `pwd`. This will run yarn install in several places - see output below." - yarn install-frontend - popd - - run: - name: Build UI Source - command: | - echo "Building src..." - pushd $PROJECT_DIR/src - echo "Current directory is: `pwd`" - echo "Running pre-fetch config: 'yarn prebuild:${STAGE}'..." - yarn prebuild:${STAGE} - echo "Running build: 'yarn build:${STAGE}'..." - yarn build:${STAGE} - popd - - run: - name: Build Edge Source - command: | - echo "Building edge..." - pushd $PROJECT_DIR/edge - echo "Current directory is: `pwd`" - echo "Running build: 'yarn build'..." - yarn build - popd - - *install-node-12 - - *install_aws_cli -# - run: -# name: Publish Console to S3 -# command: | -# pushd ${PROJECT_DIR}/src -# aws s3 rm s3://${BUCKET_NAME} --recursive -# aws s3 sync www/ s3://${BUCKET_NAME}/ --acl public-read -# popd - - run: - name: Deploy Cloudfront and LambdaEdge - command: | - pushd $PROJECT_DIR - echo "Running install 'yarn install'..." - yarn install - echo "" - echo "Running: yarn sls deploy --stage=\"${STAGE}\" --cloudfront=true" - yarn sls deploy --stage="${STAGE}" --cloudfront="true" - popd - - run: - name: Deploy Frontend Bucket - command: | - pushd $PROJECT_DIR - echo "Running install 'yarn install'..." - yarn install - echo "" - echo "Running: yarn sls client deploy --stage=\"${STAGE}\" --cloudfront=true --no-confirm --no-policy-change --no-config-change" - yarn sls client deploy --stage="${STAGE}" --cloudfront="true" --no-confirm --no-policy-change --no-config-change - popd - - run: - name: Invalidate Cache - command: | - pushd $PROJECT_DIR - echo "Running: yarn sls cloudfrontInvalidate --stage=\"${STAGE}\" --region=\"${AWS_REGION}\" --cloudfront=\"true\"" - yarn sls cloudfrontInvalidate --stage="${STAGE}" --region="${AWS_REGION}" --cloudfront="true" - popd - - # Project Management Console - deployProjectManagementConsoleDev: - <<: *deployFrontendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_DEV - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_DEV - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: dev - PROJECT_DIR: cla-frontend-project-console - ROOT_DOMAIN: lfcla.dev.platform.linuxfoundation.org - PRODUCT_DOMAIN: dev.lfcla.com - BUCKET_NAME: lf-cla-dev-cla-frontend-pmc-4 - - deployProjectManagementConsoleStaging: - <<: *deployFrontendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_STAGING - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_STAGING - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: staging - PROJECT_DIR: cla-frontend-project-console - ROOT_DOMAIN: lfcla.staging.platform.linuxfoundation.org - PRODUCT_DOMAIN: staging.lfcla.com - BUCKET_NAME: lf-cla-staging-cla-frontend-pmc-3 - - deployProjectManagementConsoleProd: - <<: *deployFrontendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_PROD - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_PROD - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: prod - PROJECT_DIR: cla-frontend-project-console - ROOT_DOMAIN: lfcla.platform.linuxfoundation.org - PRODUCT_DOMAIN: lfcla.com - BUCKET_NAME: lf-cla-prod-cla-frontend-pmc-3 - - # Corporate Console - deployCorporateConsoleDev: - <<: *deployFrontendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_DEV - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_DEV - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: dev - PROJECT_DIR: cla-frontend-corporate-console - ROOT_DOMAIN: lfcla.dev.platform.linuxfoundation.org - PRODUCT_DOMAIN: dev.lfcla.com - BUCKET_NAME: lf-cla-dev-cla-frontend-cc-4 - - deployCorporateConsoleStaging: - <<: *deployFrontendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_STAGING - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_STAGING - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: staging - PROJECT_DIR: cla-frontend-corporate-console - ROOT_DOMAIN: lfcla.staging.platform.linuxfoundation.org - PRODUCT_DOMAIN: staging.lfcla.com - BUCKET_NAME: lf-cla-staging-cla-frontend-cc-3 - - deployCorporateConsoleProd: - <<: *deployFrontendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_PROD - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_PROD - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: prod - PROJECT_DIR: cla-frontend-corporate-console - ROOT_DOMAIN: lfcla.platform.linuxfoundation.org - PRODUCT_DOMAIN: lfcla.com - BUCKET_NAME: lf-cla-prod-cla-frontend-cc-3 - - # Contributor Console - deployContributorConsoleDev: - <<: *deployFrontendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_DEV - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_DEV - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: dev - PROJECT_DIR: cla-frontend-contributor-console - ROOT_DOMAIN: lfcla.dev.platform.linuxfoundation.org - PRODUCT_DOMAIN: dev.lfcla.com - BUCKET_NAME: lf-cla-dev-cla-frontend-ic-4 - - deployContributorConsoleStaging: - <<: *deployFrontendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_STAGING - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_STAGING - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: staging - PROJECT_DIR: cla-frontend-contributor-console - ROOT_DOMAIN: lfcla.staging.platform.linuxfoundation.org - PRODUCT_DOMAIN: staging.lfcla.com - BUCKET_NAME: lf-cla-staging-cla-frontend-ic-3 - - deployContributorConsoleProd: - <<: *deployFrontendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_PROD - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_PROD - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: prod - PROJECT_DIR: cla-frontend-contributor-console - ROOT_DOMAIN: lfcla.platform.linuxfoundation.org - PRODUCT_DOMAIN: lfcla.com - BUCKET_NAME: lf-cla-prod-cla-frontend-ic-3 - - deployLandingFrontend: &deployLandingFrontendAnchor - docker: - - image: circleci/node:8-browsers - steps: - - checkout - - *setup_aws - - run: echo 'export NVM_DIR=${HOME}/.nvm' >> $BASH_ENV - - *install-node-12 - - run: - name: Install Top Level Dependencies - command: | - echo "Node version is: $(node --version)" - echo "Running top level install..." - yarn install - - run: - name: Deploy - command: | - echo "Node version is: $(node --version)" - echo "Using AWS profile: ${AWS_PROFILE}" - echo "Stage is: ${STAGE}" - echo "PROJECT_DIR=${PROJECT_DIR}" - - # Run the deploy scripts - pushd ${PROJECT_DIR} - echo "Current directory is: `pwd`" - - echo "Running install 'yarn install'..." - yarn install - echo "Running pre-fetch config: 'yarn prebuild:${STAGE}'..." - yarn prebuild:${STAGE} - echo "Running build..." - yarn build - - echo "Running deploy in folder: `pwd`" - SLS_DEBUG=* ../node_modules/serverless/bin/serverless.js deploy -s ${STAGE} -r ${AWS_REGION} --verbose - - echo "Running client deploy in folder: `pwd`" - SLS_DEBUG=* ../node_modules/serverless/bin/serverless.js client deploy -s ${STAGE} -r ${AWS_REGION} --cloudfront=true --no-confirm --no-policy-change --no-config-change - - echo "Invalidating Cloudfront caches in folder: `pwd`" - SLS_DEBUG=* ../node_modules/serverless/bin/serverless.js cloudfrontInvalidate -s ${STAGE} -r ${AWS_REGION} --cloudfront=true - popd - - no_output_timeout: 1.5h - - # Landing Page - deployLandingFrontendDev: - <<: *deployLandingFrontendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_DEV - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_DEV - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: dev - PROJECT_DIR: cla-landing-page - PRODUCT_DOMAIN: dev.lfcla.com - - deployLandingFrontendStaging: - <<: *deployLandingFrontendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_STAGING - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_STAGING - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: staging - PROJECT_DIR: cla-landing-page - PRODUCT_DOMAIN: staging.lfcla.com - - deployLandingFrontendProd: - <<: *deployLandingFrontendAnchor - environment: - AWS_ACCESS_KEY_ID_ENV_VAR: AWS_ACCESS_KEY_ID_PROD - AWS_SECRET_ACCESS_KEY_ENV_VAR: AWS_SECRET_ACCESS_KEY_PROD - AWS_PROFILE: lf-cla - AWS_REGION: us-east-1 - STAGE: prod - PROJECT_DIR: cla-landing-page - PRODUCT_DOMAIN: lfcla.com - - functionalTestsTavern: &functionalTestsTavern - docker: - - image: circleci/python:3.7.4-node - steps: - - attach_workspace: - at: ~/ - - checkout - - run: - name: setup - command: | - cd tests/rest - sudo pip3 install -r requirements.freeze.txt - echo "Installing curl and jq..." - sudo apt-get install -y curl jq - - *set_functional_test_environment - - run: - name: functional-tests - halt_build_on_fail: false # for now, we will pass all functional tests - command: | - source ${BASH_ENV} - echo "Running functional tests for stage: ${STAGE}" - cd tests/rest - tavern-ci test_*.tavern.yaml --alluredir=allure_result_folder -v || true - - functionalTestsGo: &functionalTestsGo - docker: - - image: circleci/golang:1.15.6 - steps: - - attach_workspace: - at: ~/ - - checkout - - *set_functional_test_environment - - run: - name: functional-tests-go - command: | - source "${BASH_ENV}" - echo "Running golang functional tests for stage: ${STAGE}" - echo "Home directory : $(ls ${HOME})" - echo "${HOME}/cla-backend-go directory: $(ls ${HOME}/cla-backend-go)" - ${HOME}/cla-backend-go/functional-tests - - functionalTestsTavernDev: - <<: *functionalTestsTavern - environment: - # Default Functional Test User - AUTH0_USERNAME_ENV_VAR: AUTH0_USERNAME_DEV - AUTH0_PASSWORD_ENV_VAR: AUTH0_PASSWORD_DEV - AUTH0_CLIENT_ID_ENV_VAR: AUTH0_CLIENT_ID_DEV - # Prospective CLA Manager User - AUTH0_USER1_EMAIL_ENV_VAR: AUTH0_USER1_EMAIL_DEV - AUTH0_USER1_USERNAME_ENV_VAR: AUTH0_USER1_USERNAME_DEV - AUTH0_USER1_PASSWORD_ENV_VAR: AUTH0_USER1_PASSWORD_DEV - AUTH0_USER1_CLIENT_ID_ENV_VAR: AUTH0_USER1_CLIENT_ID_DEV - # CLA Manager User - AUTH0_USER2_EMAIL_ENV_VAR: AUTH0_USER2_EMAIL_DEV - AUTH0_USER2_USERNAME_ENV_VAR: AUTH0_USER2_USERNAME_DEV - AUTH0_USER2_PASSWORD_ENV_VAR: AUTH0_USER2_PASSWORD_DEV - AUTH0_USER2_CLIENT_ID_ENV_VAR: AUTH0_USER2_CLIENT_ID_DEV - # CLA Manager Intel - AUTH0_USER3_EMAIL_ENV_VAR: AUTH0_USER3_EMAIL_DEV - AUTH0_USER3_USERNAME_ENV_VAR: AUTH0_USER3_USERNAME_DEV - AUTH0_USER3_PASSWORD_ENV_VAR: AUTH0_USER3_PASSWORD_DEV - AUTH0_USER3_CLIENT_ID_ENV_VAR: AUTH0_USER3_CLIENT_ID_DEV - # CLA Manager AT&T - AUTH0_USER4_EMAIL_ENV_VAR: AUTH0_USER4_EMAIL_DEV - AUTH0_USER4_USERNAME_ENV_VAR: AUTH0_USER4_USERNAME_DEV - AUTH0_USER4_PASSWORD_ENV_VAR: AUTH0_USER4_PASSWORD_DEV - AUTH0_USER4_CLIENT_ID_ENV_VAR: AUTH0_USER4_CLIENT_ID_DEV - # Project Manager ColorIO - AUTH0_USER5_EMAIL_ENV_VAR: AUTH0_USER5_EMAIL_DEV - AUTH0_USER5_USERNAME_ENV_VAR: AUTH0_USER5_USERNAME_DEV - AUTH0_USER5_PASSWORD_ENV_VAR: AUTH0_USER5_PASSWORD_DEV - AUTH0_USER5_CLIENT_ID_ENV_VAR: AUTH0_USER5_CLIENT_ID_DEV - API_URL: 'https://api.lfcla.dev.platform.linuxfoundation.org' - V2_API_URL: 'https://api-gw.dev.platform.linuxfoundation.org/cla-service' - STAGE: dev - - functionalTestsGoDev: - <<: *functionalTestsGo - environment: - # Default Functional Test User - AUTH0_USERNAME_ENV_VAR: AUTH0_USERNAME_DEV - AUTH0_PASSWORD_ENV_VAR: AUTH0_PASSWORD_DEV - AUTH0_CLIENT_ID_ENV_VAR: AUTH0_CLIENT_ID_DEV - # Prospective CLA Manager User - AUTH0_USER1_EMAIL_ENV_VAR: AUTH0_USER1_EMAIL_DEV - AUTH0_USER1_USERNAME_ENV_VAR: AUTH0_USER1_USERNAME_DEV - AUTH0_USER1_PASSWORD_ENV_VAR: AUTH0_USER1_PASSWORD_DEV - AUTH0_USER1_CLIENT_ID_ENV_VAR: AUTH0_USER1_CLIENT_ID_DEV - # CLA Manager User - AUTH0_USER2_EMAIL_ENV_VAR: AUTH0_USER2_EMAIL_DEV - AUTH0_USER2_USERNAME_ENV_VAR: AUTH0_USER2_USERNAME_DEV - AUTH0_USER2_PASSWORD_ENV_VAR: AUTH0_USER2_PASSWORD_DEV - AUTH0_USER2_CLIENT_ID_ENV_VAR: AUTH0_USER2_CLIENT_ID_DEV - # CLA Manager Intel - AUTH0_USER3_EMAIL_ENV_VAR: AUTH0_USER3_EMAIL_DEV - AUTH0_USER3_USERNAME_ENV_VAR: AUTH0_USER3_USERNAME_DEV - AUTH0_USER3_PASSWORD_ENV_VAR: AUTH0_USER3_PASSWORD_DEV - AUTH0_USER3_CLIENT_ID_ENV_VAR: AUTH0_USER3_CLIENT_ID_DEV - # CLA Manager AT&T - AUTH0_USER4_EMAIL_ENV_VAR: AUTH0_USER4_EMAIL_DEV - AUTH0_USER4_USERNAME_ENV_VAR: AUTH0_USER4_USERNAME_DEV - AUTH0_USER4_PASSWORD_ENV_VAR: AUTH0_USER4_PASSWORD_DEV - AUTH0_USER4_CLIENT_ID_ENV_VAR: AUTH0_USER4_CLIENT_ID_DEV - # Project Manager ColorIO - AUTH0_USER5_EMAIL_ENV_VAR: AUTH0_USER5_EMAIL_DEV - AUTH0_USER5_USERNAME_ENV_VAR: AUTH0_USER5_USERNAME_DEV - AUTH0_USER5_PASSWORD_ENV_VAR: AUTH0_USER5_PASSWORD_DEV - AUTH0_USER5_CLIENT_ID_ENV_VAR: AUTH0_USER5_CLIENT_ID_DEV - API_URL: 'https://api.lfcla.dev.platform.linuxfoundation.org' - V2_API_URL: 'https://api-gw.dev.platform.linuxfoundation.org/cla-service' - STAGE: dev - -workflows: - version: 2.1 - build_and_deploy: - jobs: - - buildBackendDev: - filters: - tags: - only: /.*/ - - buildGoBackendDev: - filters: - tags: - only: /.*/ - - buildProjectConsoleDev: - filters: - tags: - only: /.*/ - - buildCorporateConsoleDev: - filters: - tags: - only: /.*/ - - buildContributorConsoleDev: - filters: - tags: - only: /.*/ - - # Deploy Dev - - deployBackendDev: - requires: - - buildBackendDev - - buildGoBackendDev - filters: - tags: - ignore: /.*/ - branches: - only: - - main - - deployProjectManagementConsoleDev: - filters: - tags: - ignore: /.*/ - branches: - only: - - main - - deployCorporateConsoleDev: - filters: - tags: - ignore: /.*/ - branches: - only: - - main - - deployContributorConsoleDev: - filters: - tags: - ignore: /.*/ - branches: - only: - - main - - deployLandingFrontendDev: - filters: - tags: - ignore: /.*/ - branches: - only: - - main - - functionalTestsGoDev: - requires: - - deployBackendDev - filters: - tags: - ignore: /.*/ - branches: - only: - - main - - functionalTestsTavernDev: - requires: - - deployBackendDev - filters: - tags: - ignore: /.*/ - branches: - only: - - main - - # Deploy Staging - - buildBackendStaging: - filters: - branches: - ignore: /.*/ - tags: - # see semver examples https://regex101.com/r/Ly7O1x/201/ - only: /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - - buildGoBackendStaging: - filters: - branches: - ignore: /.*/ - tags: - # see semver examples https://regex101.com/r/Ly7O1x/201/ - only: /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - - approve_staging: - type: approval - requires: - - buildBackendStaging - - buildGoBackendStaging - filters: - branches: - ignore: /.*/ - tags: - # see semver examples https://regex101.com/r/Ly7O1x/201/ - only: /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - - deployBackendStaging: - requires: - - approve_staging - - buildBackendStaging - - buildGoBackendStaging - filters: - branches: - ignore: /.*/ - tags: - # see semver examples https://regex101.com/r/Ly7O1x/201/ - only: /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - - deployProjectManagementConsoleStaging: - requires: - - approve_staging - filters: - branches: - ignore: /.*/ - tags: - # see semver examples https://regex101.com/r/Ly7O1x/201/ - only: /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - - deployCorporateConsoleStaging: - requires: - - approve_staging - filters: - branches: - ignore: /.*/ - tags: - # see semver examples https://regex101.com/r/Ly7O1x/201/ - only: /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - - deployContributorConsoleStaging: - requires: - - approve_staging - filters: - branches: - ignore: /.*/ - tags: - # see semver examples https://regex101.com/r/Ly7O1x/201/ - only: /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - - deployLandingFrontendStaging: - requires: - - approve_staging - filters: - branches: - ignore: /.*/ - tags: - # see semver examples https://regex101.com/r/Ly7O1x/201/ - only: /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - - # Deploy Prod - - buildBackendProd: - filters: - branches: - ignore: /.*/ - tags: - # see semver examples https://regex101.com/r/Ly7O1x/201/ - only: /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - - buildGoBackendProd: - filters: - branches: - ignore: /.*/ - tags: - # see semver examples https://regex101.com/r/Ly7O1x/201/ - only: /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - - approve_prod: - type: approval - requires: - - buildBackendProd - - buildGoBackendProd - filters: - branches: - ignore: /.*/ - tags: - # see semver examples https://regex101.com/r/Ly7O1x/201/ - only: /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - - deployBackendProd: - requires: - - approve_prod - - buildBackendProd - - buildGoBackendProd - filters: - branches: - ignore: /.*/ - tags: - # see semver examples https://regex101.com/r/Ly7O1x/201/ - only: /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - - deployProjectManagementConsoleProd: - requires: - - approve_prod - filters: - branches: - ignore: /.*/ - tags: - # see semver examples https://regex101.com/r/Ly7O1x/201/ - only: /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - - deployCorporateConsoleProd: - requires: - - approve_prod - filters: - branches: - ignore: /.*/ - tags: - # see semver examples https://regex101.com/r/Ly7O1x/201/ - only: /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - - deployContributorConsoleProd: - requires: - - approve_prod - filters: - branches: - ignore: /.*/ - tags: - # see semver examples https://regex101.com/r/Ly7O1x/201/ - only: /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - - deployLandingFrontendProd: - requires: - - approve_prod - filters: - branches: - ignore: /.*/ - tags: - # see semver examples https://regex101.com/r/Ly7O1x/201/ - only: /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 3bbf4d3cd..8f020ed2c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -32,12 +32,20 @@ If applicable, add screenshots to help explain your problem. Please complete the following information: * Environment: + - [ ] ALL - [ ] DEV - [ ] STAGING - [ ] PROD * Browser: - [ ] Chrome/Brave - [ ] Firefox + - [ ] Opera + - [ ] Vivaldi + - [ ] LibreWolf + - [ ] SRware Iron + - [ ] Dissenter + - [ ] Slimjet + - [ ] Midori - [ ] Edge - [ ] Lynx * Version: v1.0.XX diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..de0bdfac6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,31 @@ +--- +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "npm" # See documentation for possible values + directory: "/cla-landing-page" # Location of package manifests + schedule: + interval: "weekly" + - package-ecosystem: "npm" # See documentation for possible values + directory: "/cla-backend" # Location of package manifests + schedule: + interval: "weekly" + - package-ecosystem: "pip" # See documentation for possible values + directory: "/cla-backend" # Location of package manifests + schedule: + interval: "weekly" + - package-ecosystem: "npm" # See documentation for possible values + directory: "/cla-backend-go" # Location of package manifests + schedule: + interval: "weekly" + - package-ecosystem: "gomod" # See documentation for possible values + directory: "/cla-backend-go" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml new file mode 100644 index 000000000..d7dee8ce9 --- /dev/null +++ b/.github/workflows/build-pr.yml @@ -0,0 +1,106 @@ +--- +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +name: Build and Test Pull Request +on: + pull_request: + branches: + - dev + +permissions: + id-token: write + contents: read + pull-requests: write + +env: + AWS_REGION: us-east-1 + STAGE: dev + +jobs: + build-test-lint: + runs-on: ubuntu-latest + environment: dev + steps: + - uses: actions/checkout@v3 + - name: Setup go + uses: actions/setup-go@v3 + with: + go-version: '1.20.1' + - name: Go Version + run: go version + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: '18' + - name: Setup python + uses: actions/setup-python@v4 + with: + python-version: '3.7' + cache: 'pip' + - name: Cache Go modules + uses: actions/cache@v2 + with: + path: ${{ github.workspace }}/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Configure Git to clone private Github repos + run: git config --global url."https://${TOKEN_USER}:${TOKEN}@github.com".insteadOf "https://github.com" + env: + TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN_GITHUB }} + TOKEN_USER: ${{ secrets.PERSONAL_ACCESS_TOKEN_USER_GITHUB }} + + - name: Add OS Tools + run: sudo apt update && sudo apt-get install file -y + + - name: Python Setup + working-directory: cla-backend + run: | + pip install --upgrade pip + pip install -r requirements.txt + + - name: Python Lint + working-directory: cla-backend + run: | + pylint cla/*.py || true + + - name: Python Test + working-directory: cla-backend + run: | + pytest "cla/tests" -p no:warnings + env: + PLATFORM_GATEWAY_URL: https://api-gw.dev.platform.linuxfoundation.org + AUTH0_PLATFORM_URL: https://linuxfoundation-dev.auth0.com/oauth/token + AUTH0_PLATFORM_CLIENT_ID: ${{ secrets.AUTH0_PLATFORM_CLIENT_ID }} + AUTH0_PLATFORM_CLIENT_SECRET: ${{ secrets.AUTH0_PLATFORM_CLIENT_SECRET }} + AUTH0_PLATFORM_AUDIENCE: https://api-gw.dev.platform.linuxfoundation.org/ + + - name: Go Setup + working-directory: cla-backend-go + run: | + make clean setup + + - name: Go Dependencies + working-directory: cla-backend-go + run: make deps + + - name: Go Swagger Generate + working-directory: cla-backend-go + run: | + make swagger + + - name: Go Build + working-directory: cla-backend-go + run: | + make build-lambdas-linux build-functional-tests-linux + + - name: Go Test + working-directory: cla-backend-go + run: | + make test + + - name: Go Lint + working-directory: cla-backend-go + run: make lint \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c4d7d1cbf..15fd0ab09 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,3 +1,6 @@ +--- +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT name: "CodeQL" on: @@ -24,8 +27,9 @@ jobs: # If this run was triggered by a pull request event, then checkout # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} + # Note: git checkout HEAD^2 is no longer necessary. Please remove this step as Code Scanning recommends analyzing the merge commit for best results. + #- run: git checkout HEAD^2 + # if: ${{ github.event_name == 'pull_request' }} # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 000000000..852f7ad82 --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -0,0 +1,211 @@ +--- +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +name: Build and Deploy to DEV +on: + push: + branches: + - dev + +permissions: + # These permissions are needed to interact with GitHub's OIDC Token endpoint to fetch/set the AWS deployment credentials. + id-token: write + contents: read + +env: + AWS_REGION: us-east-1 + STAGE: dev + +jobs: + build-deploy-dev: + runs-on: ubuntu-latest + environment: dev + steps: + - uses: actions/checkout@v3 + - name: Setup go + uses: actions/setup-go@v3 + with: + go-version: '1.20.1' + - name: Go Version + run: go version + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: '18' + - name: Setup python + uses: actions/setup-python@v4 + with: + python-version: '3.7' + cache: 'pip' + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + audience: sts.amazonaws.com + role-to-assume: arn:aws:iam::395594542180:role/github-actions-deploy + aws-region: us-east-1 + - name: Cache Go modules + uses: actions/cache@v2 + with: + path: ${{ github.workspace }}/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Configure Git to clone private Github repos + run: git config --global url."https://${TOKEN_USER}:${TOKEN}@github.com".insteadOf "https://github.com" + env: + TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN_GITHUB }} + TOKEN_USER: ${{ secrets.PERSONAL_ACCESS_TOKEN_USER_GITHUB }} + + - name: Add OS Tools + run: sudo apt update && sudo apt-get install file -y + + - name: Python Setup + working-directory: cla-backend + run: | + pip install --upgrade pip + pip install -r requirements.txt + + + + - name: Python Lint + working-directory: cla-backend + run: | + pylint cla/*.py || true + + - name: Python Test + working-directory: cla-backend + run: | + pytest "cla/tests" -p no:warnings + env: + PLATFORM_GATEWAY_URL: https://api-gw.dev.platform.linuxfoundation.org + AUTH0_PLATFORM_URL: https://linuxfoundation-dev.auth0.com/oauth/token + AUTH0_PLATFORM_CLIENT_ID: ${{ secrets.AUTH0_PLATFORM_CLIENT_ID }} + AUTH0_PLATFORM_CLIENT_SECRET: ${{ secrets.AUTH0_PLATFORM_CLIENT_SECRET }} + AUTH0_PLATFORM_AUDIENCE: https://api-gw.dev.platform.linuxfoundation.org/ + + - name: Go Setup + working-directory: cla-backend-go + run: | + make clean setup + + - name: Go Dependencies + working-directory: cla-backend-go + run: make deps + + - name: Go Swagger Generate + working-directory: cla-backend-go + run: | + make swagger + + - name: Go Build + working-directory: cla-backend-go + run: | + make build-lambdas-linux build-functional-tests-linux + + - name: Go Test + working-directory: cla-backend-go + run: | + make test + + - name: Go Lint + working-directory: cla-backend-go + run: make lint + + - name: Setup Deployment + working-directory: cla-backend + run: | + mkdir -p bin + cp ../cla-backend-go/bin/backend-aws-lambda bin/ + cp ../cla-backend-go/bin/user-subscribe-lambda bin/ + cp ../cla-backend-go/bin/metrics-aws-lambda bin/ + cp ../cla-backend-go/bin/metrics-report-lambda bin/ + cp ../cla-backend-go/bin/dynamo-events-lambda bin/ + cp ../cla-backend-go/bin/zipbuilder-scheduler-lambda bin/ + cp ../cla-backend-go/bin/zipbuilder-lambda bin/ + cp ../cla-backend-go/bin/gitlab-repository-check-lambda bin/ + + + - name: EasyCLA v1 Deployment us-east-1 + working-directory: cla-backend + run: | + yarn install + if [[ ! -f bin/backend-aws-lambda ]]; then echo "Missing bin/backend-aws-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/user-subscribe-lambda ]]; then echo "Missing bin/user-subscribe-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/metrics-aws-lambda ]]; then echo "Missing bin/metrics-aws-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/metrics-report-lambda ]]; then echo "Missing bin/metrics-report-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/dynamo-events-lambda ]]; then echo "Missing bin/dynamo-events-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/zipbuilder-lambda ]]; then echo "Missing bin/zipbuilder-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/zipbuilder-scheduler-lambda ]]; then echo "Missing bin/zipbuilder-scheduler-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/gitlab-repository-check-lambda ]]; then echo "Missing bin/gitlab-repository-check-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f serverless.yml ]]; then echo "Missing serverless.yml file. Exiting..."; exit 1; fi + if [[ ! -f serverless-authorizer.yml ]]; then echo "Missing serverless-authorizer.yml file. Exiting..."; exit 1; fi + yarn sls deploy --force --stage ${STAGE} --region us-east-1 --verbose + - name: EasyCLA v1 Service Check + run: | + sudo apt install curl jq -y + + # Development environment endpoints to test + declare -r v2_url="https://api.lfcla.${STAGE}.platform.linuxfoundation.org/v2/health" + declare -r v3_url="https://api.lfcla.${STAGE}.platform.linuxfoundation.org/v3/ops/health" + + echo "Validating v2 backend using endpoint: ${v2_url}" + curl --fail -XGET ${v2_url} + exit_code=$? + if [[ ${exit_coe} -eq 0 ]]; then + echo "Successful response from endpoint: ${v2_url}" + else + echo "Failed to get a successful response from endpoint: ${v2_url}" + exit ${exit_code} + fi + + echo "Validating v3 backend using endpoint: ${v3_url}" + curl --fail -XGET ${v3_url} + exit_code=$? + if [[ ${exit_coe} -eq 0 ]]; then + echo "Successful response from endpoint: ${v3_url}" + # JSON response should include "Status": "healthy" + if [[ `curl -s -XGET ${v3_url} | jq -r '.Status'` == "healthy" ]]; then + echo "Service is healthy" + else + echo "Service is NOT healthy" + exit -1 + fi + else + echo "Failed to get a successful response from endpoint: ${v3_url}" + exit ${exit_code} + fi + - name: EasyCLA v2 Deployment us-east-2 + working-directory: cla-backend-go + run: | + if [[ ! -f bin/backend-aws-lambda ]]; then echo "Missing bin/backend-aws-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/user-subscribe-lambda ]]; then echo "Missing bin/user-subscribe-lambda binary file. Exiting..."; exit 1; fi + rm -rf ./node_modules/ + yarn install + yarn sls deploy --force --stage ${STAGE} --region us-east-2 + + - name: EasyCLA v2 Service Check + run: | + sudo apt install curl jq -y + + # Development environment endpoint to test + v4_url="https://api-gw.${STAGE}.platform.linuxfoundation.org/cla-service/v4/ops/health" + + echo "Validating v4 backend using endpoint: ${v4_url}" + curl --fail -XGET ${v4_url} + exit_code=$? + if [[ ${exit_coe} -eq 0 ]]; then + echo "Successful response from endpoint: ${v4_url}" + # JSON response should include "Status": "healthy" + if [[ `curl -s -XGET ${v4_url} | jq -r '.Status'` == "healthy" ]]; then + echo "Service is healthy" + else + echo "Service is NOT healthy" + exit -1 + fi + else + echo "Failed to get a successful response from endpoint: ${v4_url}" + exit ${exit_code} + fi + diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 000000000..c8cd1a666 --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -0,0 +1,185 @@ +--- +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +name: Build and Deploy to PROD + +on: + push: + tags: + - v1.* + - v2.* + +permissions: + # These permissions are needed to interact with GitHub's OIDC Token endpoint to fetch/set the AWS deployment credentials. + id-token: write + contents: read + +env: + AWS_REGION: us-east-1 + STAGE: prod + +jobs: + build-deploy-prod: + runs-on: ubuntu-latest + environment: prod + steps: + - uses: actions/checkout@v3 + - name: Setup go + uses: actions/setup-go@v3 + with: + go-version: '1.20.1' + check-latest: true + - name: Go Version + run: go version + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: '18' + - name: Setup python + uses: actions/setup-python@v4 + with: + python-version: '3.7' + cache: 'pip' + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + audience: sts.amazonaws.com + role-to-assume: arn:aws:iam::716487311010:role/github-actions-deploy + aws-region: us-east-1 + - name: Cache Go modules + uses: actions/cache@v2 + with: + path: ${{ github.workspace }}/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Configure Git to clone private Github repos + run: git config --global url."https://${TOKEN_USER}:${TOKEN}@github.com".insteadOf "https://github.com" + env: + TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN_GITHUB }} + TOKEN_USER: ${{ secrets.PERSONAL_ACCESS_TOKEN_USER_GITHUB }} + + - name: Add OS Tools + run: sudo apt update && sudo apt-get install file -y + + - name: Python Setup + working-directory: cla-backend + run: | + pip install --upgrade pip + pip install -r requirements.txt + + - name: Go Setup + working-directory: cla-backend-go + run: | + make clean setup + + - name: Go Dependencies + working-directory: cla-backend-go + run: make deps + + - name: Go Swagger Generate + working-directory: cla-backend-go + run: | + make swagger + + - name: Go Build + working-directory: cla-backend-go + run: | + make build-lambdas-linux build-functional-tests-linux + + - name: Setup Deployment + working-directory: cla-backend + run: | + mkdir -p bin + cp ../cla-backend-go/bin/backend-aws-lambda bin/ + cp ../cla-backend-go/bin/user-subscribe-lambda bin/ + cp ../cla-backend-go/bin/metrics-aws-lambda bin/ + cp ../cla-backend-go/bin/metrics-report-lambda bin/ + cp ../cla-backend-go/bin/dynamo-events-lambda bin/ + cp ../cla-backend-go/bin/zipbuilder-scheduler-lambda bin/ + cp ../cla-backend-go/bin/zipbuilder-lambda bin/ + cp ../cla-backend-go/bin/gitlab-repository-check-lambda bin/ + + - name: EasyCLA v1 Deployment us-east-1 + working-directory: cla-backend + run: | + yarn install + if [[ ! -f bin/backend-aws-lambda ]]; then echo "Missing bin/backend-aws-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/user-subscribe-lambda ]]; then echo "Missing bin/user-subscribe-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/metrics-aws-lambda ]]; then echo "Missing bin/metrics-aws-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/metrics-report-lambda ]]; then echo "Missing bin/metrics-report-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/dynamo-events-lambda ]]; then echo "Missing bin/dynamo-events-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/zipbuilder-lambda ]]; then echo "Missing bin/zipbuilder-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/zipbuilder-scheduler-lambda ]]; then echo "Missing bin/zipbuilder-scheduler-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/gitlab-repository-check-lambda ]]; then echo "Missing bin/gitlab-repository-check-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f serverless.yml ]]; then echo "Missing serverless.yml file. Exiting..."; exit 1; fi + if [[ ! -f serverless-authorizer.yml ]]; then echo "Missing serverless-authorizer.yml file. Exiting..."; exit 1; fi + yarn sls deploy --force --stage ${STAGE} --region us-east-1 --verbose + - name: EasyCLA v1 Service Check + run: | + sudo apt install curl jq -y + + # Production environment endpoints to test + declare -r v2_url="https://api.easycla.lfx.linuxfoundation.org/v2/health" + declare -r v3_url="https://api.easycla.lfx.linuxfoundation.org/v3/ops/health" + + echo "Validating v2 backend using endpoint: ${v2_url}" + curl --fail -XGET ${v2_url} + exit_code=$? + if [[ ${exit_coe} -eq 0 ]]; then + echo "Successful response from endpoint: ${v2_url}" + else + echo "Failed to get a successful response from endpoint: ${v2_url}" + exit ${exit_code} + fi + + echo "Validating v3 backend using endpoint: ${v3_url}" + curl --fail -XGET ${v3_url} + exit_code=$? + if [[ ${exit_coe} -eq 0 ]]; then + echo "Successful response from endpoint: ${v3_url}" + # JSON response should include "Status": "healthy" + if [[ `curl -s -XGET ${v3_url} | jq -r '.Status'` == "healthy" ]]; then + echo "Service is healthy" + else + echo "Service is NOT healthy" + exit -1 + fi + else + echo "Failed to get a successful response from endpoint: ${v3_url}" + exit ${exit_code} + fi + - name: EasyCLA v2 Deployment us-east-2 + working-directory: cla-backend-go + run: | + if [[ ! -f bin/backend-aws-lambda ]]; then echo "Missing bin/backend-aws-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/user-subscribe-lambda ]]; then echo "Missing bin/user-subscribe-lambda binary file. Exiting..."; exit 1; fi + rm -rf ./node_modules/ + yarn install + yarn sls deploy --force --stage ${STAGE} --region us-east-2 + + - name: EasyCLA v2 Service Check + run: | + sudo apt install curl jq -y + + # Production environment endpoint to test + v4_url="https://api-gw.platform.linuxfoundation.org/cla-service/v4/ops/health" + + echo "Validating v4 backend using endpoint: ${v4_url}" + curl --fail -XGET ${v4_url} + exit_code=$? + if [[ ${exit_coe} -eq 0 ]]; then + echo "Successful response from endpoint: ${v4_url}" + # JSON response should include "Status": "healthy" + if [[ `curl -s -XGET ${v4_url} | jq -r '.Status'` == "healthy" ]]; then + echo "Service is healthy" + else + echo "Service is NOT healthy" + exit -1 + fi + else + echo "Failed to get a successful response from endpoint: ${v4_url}" + exit ${exit_code} + fi diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml new file mode 100644 index 000000000..ddedb5671 --- /dev/null +++ b/.github/workflows/deploy-staging.yml @@ -0,0 +1,183 @@ +--- +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +name: Build and Deploy to Staging + +on: + push: + tags: + - v1.* + - v2.* + +permissions: + # These permissions are needed to interact with GitHub's OIDC Token endpoint to fetch/set the AWS deployment credentials. + id-token: write + contents: read + +env: + AWS_REGION: us-east-1 + STAGE: staging + +jobs: + build-deploy-staging: + runs-on: ubuntu-latest + environment: staging + steps: + - uses: actions/checkout@v3 + - name: Setup go + uses: actions/setup-go@v3 + with: + go-version: '1.20.1' + check-latest: true + - name: Go Version + run: go version + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: '16' + - name: Setup python + uses: actions/setup-python@v4 + with: + python-version: '3.7' + cache: 'pip' + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + audience: sts.amazonaws.com + role-to-assume: arn:aws:iam::844390194980:role/github-actions-deploy + aws-region: us-east-1 + - name: Cache Go modules + uses: actions/cache@v2 + with: + path: ${{ github.workspace }}/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Configure Git to clone private Github repos + run: git config --global url."https://${TOKEN_USER}:${TOKEN}@github.com".insteadOf "https://github.com" + env: + TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN_GITHUB }} + TOKEN_USER: ${{ secrets.PERSONAL_ACCESS_TOKEN_USER_GITHUB }} + + - name: Add OS Tools + run: sudo apt update && sudo apt-get install file -y + + - name: Python Setup + working-directory: cla-backend + run: | + pip install -r requirements.txt + + - name: Go Setup + working-directory: cla-backend-go + run: | + make clean setup + + - name: Go Dependencies + working-directory: cla-backend-go + run: make deps + + - name: Go Swagger Generate + working-directory: cla-backend-go + run: | + make swagger + + - name: Go Build + working-directory: cla-backend-go + run: | + make build-lambdas-linux build-functional-tests-linux + + - name: Setup Deployment + working-directory: cla-backend + run: | + mkdir -p bin + cp ../cla-backend-go/bin/backend-aws-lambda bin/ + cp ../cla-backend-go/bin/user-subscribe-lambda bin/ + cp ../cla-backend-go/bin/metrics-aws-lambda bin/ + cp ../cla-backend-go/bin/metrics-report-lambda bin/ + cp ../cla-backend-go/bin/dynamo-events-lambda bin/ + cp ../cla-backend-go/bin/zipbuilder-scheduler-lambda bin/ + cp ../cla-backend-go/bin/zipbuilder-lambda bin/ + cp ../cla-backend-go/bin/gitlab-repository-check-lambda bin/ + + - name: EasyCLA v1 Deployment us-east-1 + working-directory: cla-backend + run: | + yarn install + if [[ ! -f bin/backend-aws-lambda ]]; then echo "Missing bin/backend-aws-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/user-subscribe-lambda ]]; then echo "Missing bin/user-subscribe-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/metrics-aws-lambda ]]; then echo "Missing bin/metrics-aws-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/metrics-report-lambda ]]; then echo "Missing bin/metrics-report-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/dynamo-events-lambda ]]; then echo "Missing bin/dynamo-events-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/zipbuilder-lambda ]]; then echo "Missing bin/zipbuilder-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/zipbuilder-scheduler-lambda ]]; then echo "Missing bin/zipbuilder-scheduler-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/gitlab-repository-check-lambda ]]; then echo "Missing bin/gitlab-repository-check-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f serverless.yml ]]; then echo "Missing serverless.yml file. Exiting..."; exit 1; fi + if [[ ! -f serverless-authorizer.yml ]]; then echo "Missing serverless-authorizer.yml file. Exiting..."; exit 1; fi + yarn sls deploy --force --stage ${STAGE} --region us-east-1 --verbose + - name: EasyCLA v1 Service Check + run: | + sudo apt install curl jq -y + + # Staging environment endpoints to test + declare -r v2_url="https://api.lfcla.${STAGE}.platform.linuxfoundation.org/v2/health" + declare -r v3_url="https://api.lfcla.${STAGE}.platform.linuxfoundation.org/v3/ops/health" + + echo "Validating v2 backend using endpoint: ${v2_url}" + curl --fail -XGET ${v2_url} + exit_code=$? + if [[ ${exit_coe} -eq 0 ]]; then + echo "Successful response from endpoint: ${v2_url}" + else + echo "Failed to get a successful response from endpoint: ${v2_url}" + exit ${exit_code} + fi + + echo "Validating v3 backend using endpoint: ${v3_url}" + curl --fail -XGET ${v3_url} + exit_code=$? + if [[ ${exit_coe} -eq 0 ]]; then + echo "Successful response from endpoint: ${v3_url}" + # JSON response should include "Status": "healthy" + if [[ `curl -s -XGET ${v3_url} | jq -r '.Status'` == "healthy" ]]; then + echo "Service is healthy" + else + echo "Service is NOT healthy" + exit -1 + fi + else + echo "Failed to get a successful response from endpoint: ${v3_url}" + exit ${exit_code} + fi + - name: EasyCLA v2 Deployment us-east-2 + working-directory: cla-backend-go + run: | + if [[ ! -f bin/backend-aws-lambda ]]; then echo "Missing bin/backend-aws-lambda binary file. Exiting..."; exit 1; fi + if [[ ! -f bin/user-subscribe-lambda ]]; then echo "Missing bin/user-subscribe-lambda binary file. Exiting..."; exit 1; fi + yarn install + yarn sls deploy --force --stage ${STAGE} --region us-east-2 + + - name: EasyCLA v2 Service Check + run: | + sudo apt install curl jq -y + + # Staging environment endpoint to test + v4_url="https://api-gw.${STAGE}.platform.linuxfoundation.org/cla-service/v4/ops/health" + + echo "Validating v4 backend using endpoint: ${v4_url}" + curl --fail -XGET ${v4_url} + exit_code=$? + if [[ ${exit_coe} -eq 0 ]]; then + echo "Successful response from endpoint: ${v4_url}" + # JSON response should include "Status": "healthy" + if [[ `curl -s -XGET ${v4_url} | jq -r '.Status'` == "healthy" ]]; then + echo "Service is healthy" + else + echo "Service is NOT healthy" + exit -1 + fi + else + echo "Failed to get a successful response from endpoint: ${v4_url}" + exit ${exit_code} + fi diff --git a/.github/workflows/license-header-check.yml b/.github/workflows/license-header-check.yml new file mode 100644 index 000000000..cae5515fb --- /dev/null +++ b/.github/workflows/license-header-check.yml @@ -0,0 +1,31 @@ +--- +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +name: License Header Check + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + license-header-check: + name: License Header Check + runs-on: ubuntu-latest + environment: dev + + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Check License Headers - Python + working-directory: cla-backend + run: | + ./check-headers.sh + - name: Check License Headers - Go + working-directory: cla-backend-go + run: | + ./check-headers.sh diff --git a/.github/workflows/yarn-scan-backend-go-pr.yml b/.github/workflows/yarn-scan-backend-go-pr.yml new file mode 100644 index 000000000..9e63d1246 --- /dev/null +++ b/.github/workflows/yarn-scan-backend-go-pr.yml @@ -0,0 +1,28 @@ +--- +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +name: Yarn Golang Backend Dependency Audit + +on: + # https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions + pull_request: + branches: + - dev + +jobs: + yarn-scan-backend-go-pr: + runs-on: ubuntu-latest + environment: dev + steps: + - uses: actions/checkout@v3 + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: '16' + - name: Setup + run: yarn install + - name: Yarn Audit + working-directory: cla-backend-go + run: | + yarn audit diff --git a/.github/workflows/yarn-scan-backend-pr.yml b/.github/workflows/yarn-scan-backend-pr.yml new file mode 100644 index 000000000..4f8fa851e --- /dev/null +++ b/.github/workflows/yarn-scan-backend-pr.yml @@ -0,0 +1,28 @@ +--- +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +name: Yarn Python Backend Dependency Audit + +on: + # https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions + pull_request: + branches: + - dev + +jobs: + yarn-scan-backend-pr: + runs-on: ubuntu-latest + environment: dev + steps: + - uses: actions/checkout@v3 + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: '16' + - name: Setup + run: yarn install + - name: Yarn Audit + working-directory: cla-backend + run: | + yarn audit diff --git a/.gitignore b/.gitignore index 311eba788..834c8f3bb 100755 --- a/.gitignore +++ b/.gitignore @@ -239,3 +239,5 @@ dist/* .playground api-postman/* + +cla-backend/run-python-test-example-*.py diff --git a/README.md b/README.md index 425dee4ee..a734f9d83 100644 --- a/README.md +++ b/README.md @@ -16,30 +16,45 @@ This platform supports both GitHub and Gerrit source code repositories. Addition [EasyCLA](#easycla-architecture) -Besides integration with Auth0 and Salesforce, the CLA system has the following third party services: +The EasyCL system leverages the following third party services: * [Docusign](https://www.docusign.com/) for CLA agreement e-sign flow -* [Docraptor](https://docraptor.com/) for converting html CLA template to PDF file +* [Docraptor](https://docraptor.com/) for converting CLA templates into PDF files +* [GitHub](https://github.com/) for GitHub PR CLA authorization checking/gating +* Gerrit for CLA authorization review checking/gating +* Auth0 For Single Sign On +* Salesforce via the LFX Platform APIs ## CLA Backend The CLA project has two backend components: -* The majority of the backend APIs are implemented in python, and can be found in the [cla-backend](cla-backend/) directory. - -* Recent backend development is implemented in Golang, and can be found in the -[cla-backend-go](cla-backend-go/) directory. In particular, this backend contains APIs powering -Automated Templates, GitHub Approval Lists, and Duplicate Company handling in the -Corporate Console. +* Python - some older APIs are implemented in python and can be found in the [cla-backend](cla-backend) directory. +* GoLang - Most of the backend development is implemented in Golang, and can be found in the + [cla-backend-go](cla-backend-go) directory. In particular, this backend contains APIs powering most of the v2 APIs + which integrate with the LFX Platform (including Salesforce data), and the LFX platform permissions model. ## CLA Frontend -CLA frontend consists of three independent SPA built with [Ionic](https://ionicframework.com/) framework. +For EasyCLA version 2, all three consoles are hosted in separate repositories. + +* [Project Control Center](https://projectadmin.lfx.linuxfoundation.org/) contains all the old v1 Project Console + capabilities plus many new features. This new console includes not only the EasyCLA components, but also the project + related features for LF ITX and other LFX Platform projects. +* [Corporate Console](https://organization.lfx.linuxfoundation.org/company/dashboard) contains the old v1 Company Console + capabilities. This new console includes not only the EasyCLA components, but also the company related features for LF + ITX and other LFX Platform projects. +* [Contributor Console](https://github.com/communitybridge/easycla-contributor-console) contains the old v1 Contributor Console + capabilities with new features that integrate with the LFX Platform (including the Salesforce data). + +For EasyCLA version 1, the consoles are: -* [cla-frontend-project-console](cla-frontend-project-console/) for the LinuxFoundation director/admin/user to manage project CLA -* [cla-frontend-corporate-console](cla-frontend-corporate-console/) for any concrete company CCLA manager to sign a CCLA and manage employee CLA approved list +* [cla-frontend-project-console](cla-frontend-project-console) for the LinuxFoundation director/admin/user to manage project CLA +* [cla-frontend-corporate-console](cla-frontend-corporate-console) for any concrete company CCLA manager to sign a CCLA and manage employee CLA approved list * [cla-frontend-contributor-console](cla-frontend-contributor-console) for any project contributor to sign ICLA or CCLA +These CLA frontend components of three independent SPA built with [Ionic](https://ionicframework.com/) framework. + ## EasyCLA Architecture The following diagram explains the EasyCLA architecture. @@ -58,7 +73,5 @@ Copyright The Linux Foundation and each contributor to CommunityBridge. This project’s source code is licensed under the MIT License. A copy of the license is available in LICENSE. -The project includes source code from keycloak, which is licensed under the Apache License, version 2.0 \(Apache-2.0\), a copy of which is available in LICENSE-keycloak. - This project’s documentation is licensed under the Creative Commons Attribution 4.0 International License \(CC-BY-4.0\). A copy of the license is available in LICENSE-docs. diff --git a/cla-backend-go/.gitignore b/cla-backend-go/.gitignore index b73124e8a..6e9b5b2a2 100644 --- a/cla-backend-go/.gitignore +++ b/cla-backend-go/.gitignore @@ -1,30 +1,7 @@ # Copyright The Linux Foundation and each contributor to CommunityBridge. # SPDX-License-Identifier: MIT # Project specific ignores -cla-backend-go -cla -cla-mac -backend-aws-lambda -backend-aws-lambda-mac -backend-aws-lambda-linux -user-subscribe-lambda -user-subscribe-lambda-mac -build-user-subscribe-lambda-linux -build-user-subscribe-lambda-mac -metrics-aws-lambda -metrics-aws-lambda-mac -metrics-report-lambda -metrics-report-lambda-mac -functional-tests -functional-tests-linux -functional-tests-mac -dynamo-events-lambda -dynamo-events-lambda-mac -dynamo-events-lambda-linux -zipbuilder-lambda -zipbuilder-lambda-mac -zipbuilder-scheduler-lambda-mac -zipbuilder-scheduler-lambda +bin/ *env.json db/schema.sql diff --git a/cla-backend-go/.golangci.yaml b/cla-backend-go/.golangci.yaml index d7b828cf9..737ced042 100644 --- a/cla-backend-go/.golangci.yaml +++ b/cla-backend-go/.golangci.yaml @@ -34,26 +34,27 @@ linters-settings: check-blank: true govet: check-shadowing: true - golint: + fieldalignment: true + revive: + ignore-generated-header: true min-confidence: 0 + rules: + # Recommended in Revive docs + # https://github.com/mgechev/revive#recommended-configuration + - name: package-comments + disabled: true dupl: threshold: 100 goconst: - min-len: 2 + min-len: 2 min-occurrences: 2 - maligned: - # print struct with more effective memory layout or not, false by default - suggest-new: true - + linters: disable-all: true enable: - - golint + - revive - govet - errcheck - - deadcode - - structcheck - - varcheck - ineffassign - typecheck - goconst @@ -67,8 +68,9 @@ linters: - unparam - unused - nakedret - - maligned + #- maligned # The repository of the linter has been archived by the owner. Replaced by govet 'fieldalignment'. #- dupl + - bodyclose issues: exclude-use-default: false diff --git a/cla-backend-go/Makefile b/cla-backend-go/Makefile index 10414aa88..6f64e78f8 100644 --- a/cla-backend-go/Makefile +++ b/cla-backend-go/Makefile @@ -2,15 +2,19 @@ # SPDX-License-Identifier: MIT SERVICE = cla SHELL = bash +BIN_DIR = bin LAMBDA_BIN = backend-aws-lambda METRICS_BIN = metrics-aws-lambda METRICS_REPORT_BIN = metrics-report-lambda DYNAMO_EVENTS_BIN = dynamo-events-lambda ZIPBUILDER_SCHEDULER_BIN = zipbuilder-scheduler-lambda ZIPBUILDER_BIN = zipbuilder-lambda +GITLAB_REPO_CHECK_BIN = gitlab-repository-check-lambda FUNCTIONAL_TESTS_BIN = functional-tests USER_SUBSCRIBE_BIN = user-subscribe-lambda +REPOSITORY_UPDATE_BIN = repository-update-tool MAKEFILE_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) +GOPRIVATE=github.com/LF-Engineering/* BUILD_TIME=$(shell sh -c 'date -u +%FT%T%z') VERSION := $(shell sh -c 'git describe --always --tags') BRANCH := $(shell sh -c 'git rev-parse --abbrev-ref HEAD') @@ -18,82 +22,114 @@ COMMIT := $(shell sh -c 'git rev-parse --short HEAD') LDFLAGS=-ldflags "-s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.branch=$(BRANCH) -X main.buildDate=$(BUILD_TIME)" BUILD_TAGS=-tags aws_lambda +ifeq "$(shell uname -p)" "arm" + BUILD_ARCH=arm64 +else + BUILD_ARCH=amd64 +endif +ifeq "$(shell uname -s)" "Darwin" + BUILD_HOST=darwin +endif +ifeq "$(shell uname -s)" "Linux" + BUILD_HOST=linux +endif + LINT_TOOL=$(shell go env GOPATH)/bin/golangci-lint -LINT_VERSION=v1.29.0 -SWAGGER_TOOL_VERSION=v0.24.0 +LINT_VERSION=v1.51.2 +SWAGGER_DIR=$(ROOT_DIR)/swagger +SWAGGER_BIN_DIR=/usr/local/bin +SWAGGER_TOOL_VERSION=v0.30.3 +SWAGGER_ASSET="swagger_$(BUILD_HOST)_$(BUILD_ARCH)" +SWAGGER_ASSET_URL="https://github.com/go-swagger/go-swagger/releases/download/$(SWAGGER_TOOL_VERSION)/$(SWAGGER_ASSET)" GO_PKGS=$(shell go list ./... | grep -v /vendor/ | grep -v /node_modules/) GO_FILES=$(shell find . -type f -name '*.go' -not -path './vendor/*') -TEST_ENV=AWS_REGION=us-east-1 DYNAMODB_AWS_REGION=us-east-1 AWS_PROFILE=bar AWS_ACCESS_KEY_ID=foo AWS_SECRET_ACCESS_KEY=bar -.PHONY: generate setup tool-setup setup-dev setup-deploy clean-all clean swagger up fmt test run deps build build-mac build-aws-lambda user-subscribe-lambda qc lint +.PHONY: generate setup setup-dev setup-deploy clean-all clean swagger up fmt test run deps build build-mac build-aws-lambda user-subscribe-lambda qc lint repository-update-tool all: all-mac -all-mac: clean swagger deps fmt build-mac build-aws-lambda-mac build-user-subscribe-lambda-mac build-metrics-lambda-mac build-dynamo-events-lambda-mac build-zipbuilder-scheduler-lambda-mac build-zipbuilder-lambda-mac test lint -all-linux: clean swagger deps fmt build-linux build-aws-lambda-linux build-user-subscribe-lambda-linux build-metrics-lambda-linux build-dynamo-events-lambda-linux build-zipbuilder-scheduler-lambda-linux build-zipbuilder-lambda-linux test lint -build-lambdas-mac: build-aws-lambda-mac build-user-subscribe-lambda-mac build-metrics-lambda-mac build-metrics-report-lambda-mac build-dynamo-events-lambda-mac build-zipbuilder-scheduler-lambda-mac build-zipbuilder-lambda-mac -build-lambdas-linux: build-aws-lambda-linux build-user-subscribe-lambda-linux build-metrics-lambda-linux build-metrics-report-lambda-linux build-dynamo-events-lambda-linux build-zipbuilder-scheduler-lambda-linux build-zipbuilder-lambda-linux +all-mac: clean swagger deps fmt build-mac build-aws-lambda-mac build-user-subscribe-lambda-mac build-metrics-lambda-mac build-dynamo-events-lambda-mac build-zipbuilder-scheduler-lambda-mac build-zipbuilder-lambda-mac build-gitlab-repository-check-lambda-mac build-repository-update-mac test lint +all-linux: clean swagger deps fmt build-linux build-aws-lambda-linux build-user-subscribe-lambda-linux build-metrics-lambda-linux build-dynamo-events-lambda-linux build-zipbuilder-scheduler-lambda-linux build-zipbuilder-lambda-linux build-gitlab-repository-check-lambda-linux build-repository-update-linux test lint +lambdas-mac: build-lambdas-mac +build-lambdas-mac: build-aws-lambda-mac build-user-subscribe-lambda-mac build-metrics-lambda-mac build-metrics-report-lambda-mac build-dynamo-events-lambda-mac build-zipbuilder-scheduler-lambda-mac build-zipbuilder-lambda-mac build-gitlab-repository-check-lambda-mac +lambdas: build-lambdas-linux +build-lambdas-linux: build-aws-lambda-linux build-user-subscribe-lambda-linux build-metrics-lambda-linux build-metrics-report-lambda-linux build-dynamo-events-lambda-linux build-zipbuilder-scheduler-lambda-linux build-zipbuilder-lambda-linux build-gitlab-repository-check-lambda-linux generate: swagger -setup: $(LINT_TOOL) setup-dev setup-deploy +setup: setup-dev setup-swagger setup-deploy -tool-setup: - @echo "Installing gobin for installing tools..." - @# gobin is the equivalent of 'go get' whilst in module-aware mode but this does not modify your go.mod - GO111MODULE=off go get -u github.com/myitcv/gobin +.PHONY: setup-swagger +setup-swagger: + @echo "==> Removing old swagger binary in $(SWAGGER_BIN_DIR)..." + @sudo rm -Rf $(SWAGGER_BIN_DIR)/swagger + @echo "==> Downloading $(SWAGGER_ASSET_URL)..." + sudo curl -o $(SWAGGER_BIN_DIR)/swagger -L'#' $(SWAGGER_ASSET_URL) + sudo chmod +x $(SWAGGER_BIN_DIR)/swagger + $(SWAGGER_BIN_DIR)/swagger version setup_dev: setup-dev -setup-dev: tool-setup - @echo "Removing previously install version of swagger..." - @rm -Rf $(shell echo $(GOPATH))/bin/swagger $(shell echo $(GOPATH))/src/github.com/go-swagger - @echo "Installing swagger version: '$(SWAGGER_TOOL_VERSION)'..." - gobin github.com/go-swagger/go-swagger/cmd/swagger@$(SWAGGER_TOOL_VERSION) - @echo "Installing goimports..." - gobin golang.org/x/tools/cmd/goimports - @echo "Installing cover..." - gobin golang.org/x/tools/cmd/cover - @echo "Installing multi-file-swagger tool..." - cd $(dir $(realpath $(firstword $(MAKEFILE_LIST))))swagger && pip3 install virtualenv && virtualenv .venv && source .venv/bin/activate && pip3 install -r requirements.txt +setup-dev: + pushd /tmp && echo "==> Installing goimport..." && go install golang.org/x/tools/cmd/goimports@v0.24.0 && echo "==> Installation coverage tools..." && go install golang.org/x/tools/cmd/cover@latest && popd + + @echo "==> Installing linter..." + @# Latest releases: https://github.com/golangci/golangci-lint/releases + go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(LINT_VERSION) + echo "golangci-lint version:" && golangci-lint version + + @echo "==> Installing multi-file-swagger tool..." + cd $(dir $(realpath $(firstword $(MAKEFILE_LIST))))swagger && pip3 install virtualenv && virtualenv .venv && source .venv/bin/activate && python -m pip install --upgrade pip && pip3 install -r requirements.txt setup_deploy: setup-deploy setup-deploy: - @yarn add serverless && yarn install + @yarn install + +clean: clean-models clean-lambdas + @rm -rf cla cla-mac* cla-linux -clean: - @rm -rf cla cla-mac cla-linux \ - ./v2/project-service/client ./v2/project-service/models \ +clean-models: + @rm -rf ./v2/project-service/client ./v2/project-service/models \ ./v2/organization-service/client ./v2/organization-service/models \ - ./v2/user-service/client ./v2/user-service/models \ - backend-aws-lambda* dynamo-events-lambda* \ - functional-tests* metrics-aws-lambda* metrics-report-lambda* \ - user-subscribe-lambda* zipbuild-lambda* zipbuilder-scheduler-lambda* + ./v2/user-service/client ./v2/user-service/models +clean-lambdas: + @rm -rf $(BIN_DIR) + +swagger-clean: clean-swagger clean-swagger: @rm -rf gen/ clean-all: clean clean-swagger @rm -rf vendor/ -swagger: clean-swagger swagger-build swagger-validate +swagger: clean-swagger swagger-prep swagger-build swagger-validate +build-swagger: swagger-build +swagger-build: swagger-build-v1-services swagger-build-v2-services swagger-build-project-service swagger-build-organization-service swagger-build-user-service swagger-build-acs-service +swagger-validate: swagger-v1-validate swagger-v2-validate swagger-prep: @mkdir gen swagger-build-v1-services: @echo - @echo "Generating v1 legacy API models..." + @echo "==> Swagger version is: $(shell $(SWAGGER_BIN_DIR)/swagger version )" + @echo "==> Go version is: $(shell go version )" + @echo "==> Generating v1 legacy API models..." cd swagger; source .venv/bin/activate && python3 multi-file-swagger.py --spec-input-file cla.v1.yaml --spec-output-file cla.v1.compiled.yaml swagger -q generate server \ -t gen \ -f swagger/cla.v1.compiled.yaml \ --copyright-file=copyright-header.txt \ + --server-package=v1/restapi \ + --model-package=v1/models \ --exclude-main \ -A cla \ - -P user.CLAUser + -P github.com/communitybridge/easycla/cla-backend-go/user.CLAUser swagger-build-v2-services: @echo - @echo "Generating v2 API models..." + @echo "==> Swagger version is: $(shell $(SWAGGER_BIN_DIR)/swagger version )" + @echo "==> Go version is: $(shell go version )" + @echo "==> Generating v2 API models..." cd swagger; source .venv/bin/activate && python3 multi-file-swagger.py --spec-input-file cla.v2.yaml --spec-output-file cla.v2.compiled.yaml swagger -q generate server \ -t gen \ @@ -103,44 +139,55 @@ swagger-build-v2-services: --model-package=v2/models \ --exclude-main \ -A easycla \ - -P auth.User + -P github.com/LF-Engineering/lfx-kit/auth.User swagger-build-project-service: @echo - @echo "Generating swagger client for the project-service..." + @echo "==> Swagger version is: $(shell $(SWAGGER_BIN_DIR)/swagger version )" + @echo "==> Go version is: $(shell go version )" + @echo "==> Generating swagger client for the project-service..." @mkdir -p v2/project-service curl -sfL https://api-gw.dev.platform.linuxfoundation.org/project-service/swagger.json --output swagger/project-service.yaml sed -i.bak 's/X-ACL/Empty-Header/g' swagger/project-service.yaml swagger -q generate client \ --copyright-file=copyright-header.txt \ -t v2/project-service \ - -f swagger/project-service.yaml + -f swagger/project-service.yaml \ + --skip-validation # needed, currently seeing: body.default.Filename in body must be of type string: "null", and definitions.artifact-upload-init-request.default.Filename in body must be of type string: "null" issues, notified PS team swagger-build-organization-service: @echo - @echo "Generating swagger client for the organization-service..." + @echo "==> Swagger version is: $(shell $(SWAGGER_BIN_DIR)/swagger version )" + @echo "==> Go version is: $(shell go version )" + @echo "==> Generating swagger client for the organization-service..." @mkdir -p v2/organization-service curl -sfL https://api-gw.dev.platform.linuxfoundation.org/organization-service/swagger.json --output swagger/organization-service.yaml sed -i.bak 's/X-ACL/Empty-Header/g' swagger/organization-service.yaml swagger -q generate client \ --copyright-file=copyright-header.txt \ -t v2/organization-service \ - -f swagger/organization-service.yaml + -f swagger/organization-service.yaml \ + --skip-validation # needed, currently seeing: - username in query must be of type string: "null" swagger-build-user-service: @echo - @echo "Generating swagger client for the user-service..." + @echo "==> Swagger version is: $(shell $(SWAGGER_BIN_DIR)/swagger version )" + @echo "==> Go version is: $(shell go version )" + @echo "==> Generating swagger client for the user-service..." @mkdir -p v2/user-service curl -sfL https://api-gw.dev.platform.linuxfoundation.org/user-service/swagger.json --output swagger/user-service.yaml sed -i.bak 's/X-ACL/Empty-Header/g' swagger/user-service.yaml swagger -q generate client \ --copyright-file=copyright-header.txt \ -t v2/user-service \ - -f swagger/user-service.yaml + -f swagger/user-service.yaml \ + --skip-validation # needed, many validation errors swagger-build-acs-service: @echo - @echo "Generating swagger client for the acs-service..." + @echo "==> Swagger version is: $(shell $(SWAGGER_BIN_DIR)/swagger version )" + @echo "==> Go version is: $(shell go version )" + @echo "==> Generating swagger client for the acs-service..." @mkdir -p v2/acs-service curl -sfL https://api-gw.dev.platform.linuxfoundation.org/acs/v1/api-docs/swagger/swagger.json --output swagger/acs-service.yaml sed -i.bak 's/X-ACL/X-API-KEY/g' swagger/acs-service.yaml @@ -149,151 +196,159 @@ swagger-build-acs-service: -t v2/acs-service \ -f swagger/acs-service.yaml -swagger-build: clean-swagger swagger-prep swagger-build-v1-services swagger-build-v2-services swagger-build-project-service swagger-build-organization-service swagger-build-user-service swagger-build-acs-service - -swagger-validate: swagger-v1-validate swagger-v2-validate - swagger-v1-validate: @echo "" - @echo "Validating EasyCLA v1 legacy API specification..." + @echo "==> Validating EasyCLA v1 legacy API specification..." @swagger validate --stop-on-error swagger/cla.v1.compiled.yaml swagger-v2-validate: @echo "" - @echo "Validating EasyCLA v2 API specification..." + @echo "==> Validating EasyCLA v2 API specification..." @swagger validate --stop-on-error swagger/cla.v2.compiled.yaml fmt: - @echo "Formatting code and optimizing imports..." + @echo "==> Formatting code and optimizing imports..." @gofmt -w -l -s $(GO_FILES) @goimports -w -l $(GO_FILES) test: - @echo "Running unit tests..." - @ $(TEST_ENV) go test -v $(shell go list ./... | grep -v /vendor/ | grep -v /node_modules/) -coverprofile=cover.out + @echo "==> Running unit tests..." + @go test -v $(shell go list ./... | grep -v /vendor/ | grep -v /node_modules/) -coverprofile=cover.out + @echo "==> Unit test successful!" mock: - @echo "Re-Generating mocks" - @cd $(MAKEFILE_DIR) && mkdir -p repositories/mock - @cd $(MAKEFILE_DIR) && mockgen -copyright_file=copyright-header.txt -source=repositories/service.go -package=mock -destination=repositories/mock/mock_service.go - @cd $(MAKEFILE_DIR) && mockgen -copyright_file=copyright-header.txt -source=repositories/repository.go -package=mock -destination=repositories/mock/mock_repository.go - -run: - go run main.go + @echo "==> Re-Generating mocks" + @./tools/regenmocks.sh deps: - go env -w GOPRIVATE=github.com/LF-Engineering/* - go mod download + @go env -w GOPRIVATE=$(GOPRIVATE) + @go mod download -x + +build-prep: + @mkdir -p $(BIN_DIR) build: build-linux -build-linux: deps - @echo "Building Linux amd64 binary..." - env GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(SERVICE) main.go - @chmod +x $(SERVICE) +build-linux: deps build-prep + @echo "==> Building Linux amd64 binary..." + env GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BIN_DIR)/$(SERVICE) main.go + @chmod +x $(BIN_DIR)/$(SERVICE) -build-mac: deps - @echo "Building Mac OSX amd64 binary..." - env GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(SERVICE)-mac main.go - @chmod +x $(SERVICE)-mac +build-mac: deps build-prep + @echo "==> Building Mac OSX $(BUILD_ARCH) binary..." + env GOOS=darwin GOARCH=$(BUILD_ARCH) go build $(LDFLAGS) -o $(BIN_DIR)/$(SERVICE)-mac main.go + @chmod +x $(BIN_DIR)/$(SERVICE)-mac rebuild-mac: clean fmt build-mac lint - ./$(SERVICE)-mac build-aws-lambda: build-aws-lambda-linux -build-aws-lambda-linux: deps - @echo "Building a statically linked Linux amd64 binary..." - env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) $(BUILD_TAGS) -o $(LAMBDA_BIN) main.go - @chmod +x $(LAMBDA_BIN) +build-aws-lambda-linux: deps build-prep + @echo "==> Building a statically linked Linux amd64 binary..." + env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) $(BUILD_TAGS) -o $(BIN_DIR)/$(LAMBDA_BIN) main.go + @chmod +x $(BIN_DIR)/$(LAMBDA_BIN) -build-aws-lambda-mac: deps - @echo "Building a statically linked Mac OSX amd64 binary..." - env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) $(BUILD_TAGS) -o $(LAMBDA_BIN)-mac main.go - @chmod +x $(LAMBDA_BIN)-mac +build-aws-lambda-mac: deps build-prep + @echo "==> Building a statically linked Mac OSX amd64 binary..." + env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) $(BUILD_TAGS) -o $(BIN_DIR)/$(LAMBDA_BIN)-mac main.go + @chmod +x $(BIN_DIR)/$(LAMBDA_BIN)-mac build-user-subscribe-lambda: build-user-subscribe-lambda-linux -build-user-subscribe-lambda-linux: deps - @echo "Building a statically linked Linux amd64 binary..." - env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) $(BUILD_TAGS) -o $(USER_SUBSCRIBE_BIN) userSubscribeLambda/main.go - @chmod +x $(USER_SUBSCRIBE_BIN) +build-user-subscribe-lambda-linux: deps build-prep + @echo "==> Building a statically linked Linux amd64 binary..." + env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) $(BUILD_TAGS) -o $(BIN_DIR)/$(USER_SUBSCRIBE_BIN) cmd/user-subscribe-lambda/main.go + @chmod +x $(BIN_DIR)/$(USER_SUBSCRIBE_BIN) -build-user-subscribe-lambda-mac: deps - @echo "Building a statically linked Mac OSX amd64 binary..." - env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) $(BUILD_TAGS) -o $(USER_SUBSCRIBE_BIN)-mac userSubscribeLambda/main.go - @chmod +x $(USER_SUBSCRIBE_BIN)-mac +build-user-subscribe-lambda-mac: deps build-prep + @echo "==> Building a statically linked Mac OSX amd64 binary..." + env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) $(BUILD_TAGS) -o $(BIN_DIR)/$(USER_SUBSCRIBE_BIN)-mac cmd/user-subscribe-lambda/main.go + @chmod +x $(BIN_DIR)/$(USER_SUBSCRIBE_BIN)-mac build-metrics-lambda: build-metrics-lambda-linux -build-metrics-lambda-linux: deps - @echo "Building a statically linked Linux amd64 binary..." - env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(METRICS_BIN) cmd/metrics_lambda/main.go - @chmod +x $(METRICS_BIN) +build-metrics-lambda-linux: deps build-prep + @echo "==> Building a statically linked Linux amd64 binary..." + env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BIN_DIR)/$(METRICS_BIN) cmd/metrics_lambda/main.go + @chmod +x $(BIN_DIR)/$(METRICS_BIN) -build-metrics-lambda-mac: deps - @echo "Building a statically linked Mac OSX amd64 binary..." - env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(METRICS_BIN)-mac cmd/metrics_lambda/main.go - @chmod +x $(METRICS_BIN)-mac +build-metrics-lambda-mac: deps build-prep + @echo "==> Building a statically linked Mac OSX amd64 binary..." + env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BIN_DIR)/$(METRICS_BIN)-mac cmd/metrics_lambda/main.go + @chmod +x $(BIN_DIR)/$(METRICS_BIN)-mac build-metrics-report-lambda: build-metrics-report-lambda-linux -build-metrics-report-lambda-linux: deps - @echo "Building a statically linked Linux amd64 binary..." - env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(METRICS_REPORT_BIN) cmd/metrics_report_lambda/main.go - @chmod +x $(METRICS_REPORT_BIN) - -build-metrics-report-lambda-mac: deps - @echo "Building a statically linked Mac OSX amd64 binary..." - env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(METRICS_REPORT_BIN)-mac cmd/metrics_report_lambda/main.go - @chmod +x $(METRICS_REPORT_BIN)-mac +build-metrics-report-lambda-linux: deps build-prep + @echo "==> Building a statically linked Linux amd64 binary..." + env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BIN_DIR)/$(METRICS_REPORT_BIN) cmd/metrics_report_lambda/main.go + @chmod +x $(BIN_DIR)/$(METRICS_REPORT_BIN) +build-metrics-report-lambda-mac: deps build-prep + @echo "==> Building a statically linked Mac OSX amd64 binary..." + env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BIN_DIR)/$(METRICS_REPORT_BIN)-mac cmd/metrics_report_lambda/main.go + @chmod +x $(BIN_DIR)/$(METRICS_REPORT_BIN)-mac build-dynamo-events-lambda: build-dynamo-events-lambda-linux -build-dynamo-events-lambda-linux: deps - @echo "Building a statically linked Linux amd64 binary..." - env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(DYNAMO_EVENTS_BIN) cmd/dynamo_events_lambda/main.go - @chmod +x $(DYNAMO_EVENTS_BIN) +build-dynamo-events-lambda-linux: deps build-prep + @echo "==> Building a statically linked Linux amd64 binary..." + env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BIN_DIR)/$(DYNAMO_EVENTS_BIN) cmd/dynamo_events_lambda/main.go + @chmod +x $(BIN_DIR)/$(DYNAMO_EVENTS_BIN) -build-dynamo-events-lambda-mac: deps - @echo "Building a statically linked Mac OSX amd64 binary..." - env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(DYNAMO_EVENTS_BIN)-mac cmd/dynamo_events_lambda/main.go - @chmod +x $(DYNAMO_EVENTS_BIN)-mac +build-dynamo-events-lambda-mac: deps build-prep + @echo "==> Building a statically linked Mac OSX amd64 binary..." + env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BIN_DIR)/$(DYNAMO_EVENTS_BIN)-mac cmd/dynamo_events_lambda/main.go + @chmod +x $(BIN_DIR)/$(DYNAMO_EVENTS_BIN)-mac build-zipbuilder-scheduler-lambda: build-zipbuilder-scheduler-lambda-linux -build-zipbuilder-scheduler-lambda-linux: deps - @echo "Building a statically linked Linux amd64 binary..." - env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(ZIPBUILDER_SCHEDULER_BIN) cmd/zipbuilder_scheduler_lambda/main.go - @chmod +x $(ZIPBUILDER_SCHEDULER_BIN) +build-zipbuilder-scheduler-lambda-linux: deps build-prep + @echo "==> Building a statically linked Linux amd64 binary..." + env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BIN_DIR)/$(ZIPBUILDER_SCHEDULER_BIN) cmd/zipbuilder_scheduler_lambda/main.go + @chmod +x $(BIN_DIR)/$(ZIPBUILDER_SCHEDULER_BIN) -build-zipbuilder-scheduler-lambda-mac: deps - @echo "Building a statically linked Mac OSX amd64 binary..." - env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(ZIPBUILDER_SCHEDULER_BIN)-mac cmd/zipbuilder_scheduler_lambda/main.go - @chmod +x $(ZIPBUILDER_SCHEDULER_BIN)-mac +build-zipbuilder-scheduler-lambda-mac: deps build-prep + @echo "==> Building a statically linked Mac OSX amd64 binary..." + env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BIN_DIR)/$(ZIPBUILDER_SCHEDULER_BIN)-mac cmd/zipbuilder_scheduler_lambda/main.go + @chmod +x $(BIN_DIR)/$(ZIPBUILDER_SCHEDULER_BIN)-mac build-zipbuilder-lambda: build-zipbuilder-lambda-linux -build-zipbuilder-lambda-linux: deps - @echo "Building a statically linked Linux amd64 binary..." - env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(ZIPBUILDER_BIN) cmd/zipbuilder_lambda/main.go - @chmod +x $(ZIPBUILDER_BIN) - -build-zipbuilder-lambda-mac: deps - @echo "Building a statically linked Mac OSX amd64 binary..." - env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(ZIPBUILDER_BIN)-mac cmd/zipbuilder_lambda/main.go - @chmod +x $(ZIPBUILDER_BIN)-mac +build-zipbuilder-lambda-linux: deps build-prep + @echo "==> Building a statically linked Linux amd64 binary..." + env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BIN_DIR)/$(ZIPBUILDER_BIN) cmd/zipbuilder_lambda/main.go + @chmod +x $(BIN_DIR)/$(ZIPBUILDER_BIN) + +build-zipbuilder-lambda-mac: deps build-prep + @echo "==> Building a statically linked Mac OSX amd64 binary..." + env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BIN_DIR)/$(ZIPBUILDER_BIN)-mac cmd/zipbuilder_lambda/main.go + @chmod +x $(BIN_DIR)/$(ZIPBUILDER_BIN)-mac + +build-gitlab-repository-check-lambda-linux: deps build-prep + @echo "==> Building a statically linked Linux OSX amd64 binary..." + env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) $(BUILD_TAGS) -o $(BIN_DIR)/$(GITLAB_REPO_CHECK_BIN) cmd/gitlab_repository_check/main.go + @chmod +x $(BIN_DIR)/$(GITLAB_REPO_CHECK_BIN) + +build-gitlab-repository-check-lambda-mac: deps build-prep + @echo "==> Building a statically linked Mac OSX amd64 binary..." + env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BIN_DIR)/$(GITLAB_REPO_CHECK_BIN)-mac cmd/gitlab_repository_check/main.go + @chmod +x $(BIN_DIR)/$(GITLAB_REPO_CHECK_BIN)-mac build-functional-tests: build-functional-tests-linux -build-functional-tests-linux: deps - @echo "Building Functional Tests for Linux amd64 binary..." - env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(FUNCTIONAL_TESTS_BIN) cmd/functional_tests/main.go - @chmod +x $(FUNCTIONAL_TESTS_BIN) - -build-functional-tests-mac: deps - @echo "Building Functional Tests for OSX amd64 binary..." - env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(FUNCTIONAL_TESTS_BIN)-mac cmd/functional_tests/main.go - @chmod +x $(FUNCTIONAL_TESTS_BIN)-mac - -$(LINT_TOOL): - @echo "Downloading golangci-lint version $(LINT_VERSION)..." - @# Latest releases: https://github.com/golangci/golangci-lint/releases - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(shell go env GOPATH)/bin $(LINT_VERSION) - -lint: $(LINT_TOOL) - @cd $(MAKEFILE_DIR) && echo "Running lint..." && $(LINT_TOOL) run --allow-parallel-runners --config=.golangci.yaml ./... && echo "Lint check passed." +build-functional-tests-linux: deps build-prep + @echo "==> Building Functional Tests for Linux amd64 binary..." + env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BIN_DIR)/$(FUNCTIONAL_TESTS_BIN) cmd/functional_tests/main.go + @chmod +x $(BIN_DIR)/$(FUNCTIONAL_TESTS_BIN) + +build-functional-tests-mac: deps build-prep + @echo "==> Building Functional Tests for OSX amd64 binary..." + env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BIN_DIR)/$(FUNCTIONAL_TESTS_BIN)-mac cmd/functional_tests/main.go + @chmod +x $(BIN_DIR)/$(FUNCTIONAL_TESTS_BIN)-mac + +build-repository-update: build-repository-update-linux +build-repository-update-linux: deps build-prep + @echo "==> Building a statically linked Linux amd64 binary..." + env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BIN_DIR)/$(REPOSITORY_UPDATE_BIN) cmd/repository_project_update/main.go + @chmod +x $(BIN_DIR)/$(REPOSITORY_UPDATE_BIN) + +build-repository-update-mac: deps build-prep + @echo "==> Building a statically linked Mac OSX amd64 binary..." + env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BIN_DIR)/$(REPOSITORY_UPDATE_BIN)-mac cmd/repository_project_update/main.go + @chmod +x $(BIN_DIR)/$(REPOSITORY_UPDATE_BIN)-mac + +lint: + @cd $(MAKEFILE_DIR) && $(LINT_TOOL) version && echo "==> Running lint..." && $(LINT_TOOL) run --exclude="this method will not auto-escape HTML. Verify data is well formed" --allow-parallel-runners --config=.golangci.yaml ./... && echo "==> Lint check passed." @cd $(MAKEFILE_DIR) && ./check-headers.sh - diff --git a/cla-backend-go/api_client/api_client.go b/cla-backend-go/api_client/api_client.go new file mode 100644 index 000000000..42c0d1999 --- /dev/null +++ b/cla-backend-go/api_client/api_client.go @@ -0,0 +1,27 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package apiclient + +import ( + "context" + "net/http" +) + +type APIClient interface { + GetData(ctx context.Context, url string) (*http.Response, error) +} + +type RestAPIClient struct { + Client *http.Client +} + +// GetData makes a get request to the specified url + +func (c *RestAPIClient) GetData(ctx context.Context, url string) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + return c.Client.Do(req) +} diff --git a/cla-backend-go/api_client/mocks/mock_client.go b/cla-backend-go/api_client/mocks/mock_client.go new file mode 100644 index 000000000..08dd5ceba --- /dev/null +++ b/cla-backend-go/api_client/mocks/mock_client.go @@ -0,0 +1,54 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +// Code generated by MockGen. DO NOT EDIT. +// Source: api_client/api_client.go + +// Package mock_apiclient is a generated GoMock package. +package mock_apiclient + +import ( + context "context" + http "net/http" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockAPIClient is a mock of APIClient interface. +type MockAPIClient struct { + ctrl *gomock.Controller + recorder *MockAPIClientMockRecorder +} + +// MockAPIClientMockRecorder is the mock recorder for MockAPIClient. +type MockAPIClientMockRecorder struct { + mock *MockAPIClient +} + +// NewMockAPIClient creates a new mock instance. +func NewMockAPIClient(ctrl *gomock.Controller) *MockAPIClient { + mock := &MockAPIClient{ctrl: ctrl} + mock.recorder = &MockAPIClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAPIClient) EXPECT() *MockAPIClientMockRecorder { + return m.recorder +} + +// GetData mocks base method. +func (m *MockAPIClient) GetData(ctx context.Context, url string) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetData", ctx, url) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetData indicates an expected call of GetData. +func (mr *MockAPIClientMockRecorder) GetData(ctx, url interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetData", reflect.TypeOf((*MockAPIClient)(nil).GetData), ctx, url) +} diff --git a/cla-backend-go/approval_list/handlers.go b/cla-backend-go/approval_list/handlers.go index 94193a314..8dfafddbc 100644 --- a/cla-backend-go/approval_list/handlers.go +++ b/cla-backend-go/approval_list/handlers.go @@ -9,9 +9,9 @@ import ( "github.com/sirupsen/logrus" "github.com/communitybridge/easycla/cla-backend-go/events" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/company" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/company" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/signatures" "github.com/communitybridge/easycla/cla-backend-go/user" @@ -27,12 +27,12 @@ func Configure(api *operations.ClaAPI, service IService, sessionStore *dynastore func(params company.AddCclaWhitelistRequestParams) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint - requestID, err := service.AddCclaWhitelistRequest(ctx, params.CompanyID, params.ProjectID, params.Body) + requestID, err := service.AddCclaApprovalListRequest(ctx, params.CompanyID, params.ProjectID, params.Body) if err != nil { return company.NewAddCclaWhitelistRequestBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(err)) } - eventsService.LogEvent(&events.LogEventArgs{ + eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.CCLAApprovalListRequestCreated, ProjectID: params.ProjectID, CompanyID: params.CompanyID, @@ -47,12 +47,12 @@ func Configure(api *operations.ClaAPI, service IService, sessionStore *dynastore func(params company.ApproveCclaWhitelistRequestParams, claUser *user.CLAUser) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint - err := service.ApproveCclaWhitelistRequest(ctx, params.CompanyID, params.ProjectID, params.RequestID) + err := service.ApproveCclaApprovalListRequest(ctx, claUser, params.CompanyID, params.ProjectID, params.RequestID) if err != nil { return company.NewApproveCclaWhitelistRequestBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(err)) } - eventsService.LogEvent(&events.LogEventArgs{ + eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.CCLAApprovalListRequestApproved, ProjectID: params.ProjectID, CompanyID: params.CompanyID, @@ -67,12 +67,12 @@ func Configure(api *operations.ClaAPI, service IService, sessionStore *dynastore func(params company.RejectCclaWhitelistRequestParams, claUser *user.CLAUser) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint - err := service.RejectCclaWhitelistRequest(ctx, params.CompanyID, params.ProjectID, params.RequestID) + err := service.RejectCclaApprovalListRequest(ctx, params.CompanyID, params.ProjectID, params.RequestID) if err != nil { return company.NewRejectCclaWhitelistRequestBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(err)) } - eventsService.LogEvent(&events.LogEventArgs{ + eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.CCLAApprovalListRequestRejected, ProjectID: params.ProjectID, CompanyID: params.CompanyID, @@ -91,9 +91,9 @@ func Configure(api *operations.ClaAPI, service IService, sessionStore *dynastore "functionName": "CompanyListCclaWhitelistRequestsHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), } - log.WithFields(f).Debugf("Invoking ListCclaWhitelistRequest with Company ID: %+v, Project ID: %+v, Status: %+v", + log.WithFields(f).Debugf("Invoking ListCclaApprovalListRequests with Company ID: %+v, Project ID: %+v, Status: %+v", params.CompanyID, params.ProjectID, params.Status) - result, err := service.ListCclaWhitelistRequest(params.CompanyID, params.ProjectID, params.Status) + result, err := service.ListCclaApprovalListRequest(params.CompanyID, params.ProjectID, params.Status) if err != nil { return company.NewListCclaWhitelistRequestsBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(err)) } @@ -103,9 +103,22 @@ func Configure(api *operations.ClaAPI, service IService, sessionStore *dynastore api.CompanyListCclaWhitelistRequestsByCompanyAndProjectHandler = company.ListCclaWhitelistRequestsByCompanyAndProjectHandlerFunc( func(params company.ListCclaWhitelistRequestsByCompanyAndProjectParams, claUser *user.CLAUser) middleware.Responder { - log.Debugf("Invoking ListCclaWhitelistRequestByCompanyProjectUser with Company ID: %+v, Project ID: %+v, Status: %+v", + reqID := utils.GetRequestID(params.XREQUESTID) + ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + f := logrus.Fields{ + "functionName": "v1.approval_list.handlers.CompanyListCclaWhitelistRequestsByCompanyAndProjectHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companyID": params.CompanyID, + "projectID": params.ProjectID, + "status": utils.StringValue(params.Status), + "claUserName": claUser.Name, + "claUserUserID": claUser.UserID, + "claUserLFEmail": claUser.LFEmail, + "claUserLFUsername": claUser.LFUsername, + } + log.WithFields(f).Debugf("Invoking ListCclaApprovalListRequestByCompanyProjectUser with Company ID: %+v, Project ID: %+v, Status: %+v", params.CompanyID, params.ProjectID, params.Status) - result, err := service.ListCclaWhitelistRequestByCompanyProjectUser(params.CompanyID, ¶ms.ProjectID, params.Status, nil) + result, err := service.ListCclaApprovalListRequestByCompanyProjectUser(params.CompanyID, ¶ms.ProjectID, params.Status, nil) if err != nil { return company.NewListCclaWhitelistRequestsByCompanyAndProjectBadRequest().WithPayload(errorResponse(err)) } @@ -115,9 +128,9 @@ func Configure(api *operations.ClaAPI, service IService, sessionStore *dynastore api.CompanyListCclaWhitelistRequestsByCompanyAndProjectAndUserHandler = company.ListCclaWhitelistRequestsByCompanyAndProjectAndUserHandlerFunc( func(params company.ListCclaWhitelistRequestsByCompanyAndProjectAndUserParams, claUser *user.CLAUser) middleware.Responder { - log.Debugf("Invoking ListCclaWhitelistRequestByCompanyProjectUser with Company ID: %+v, Project ID: %+v, Status: %+v, User: %+v", + log.Debugf("Invoking ListCclaApprovalListRequestByCompanyProjectUser with Company ID: %+v, Project ID: %+v, Status: %+v, User: %+v", params.CompanyID, params.ProjectID, params.Status, claUser.LFUsername) - result, err := service.ListCclaWhitelistRequestByCompanyProjectUser(params.CompanyID, ¶ms.ProjectID, params.Status, &claUser.LFUsername) + result, err := service.ListCclaApprovalListRequestByCompanyProjectUser(params.CompanyID, ¶ms.ProjectID, params.Status, &claUser.LFUsername) if err != nil { return company.NewListCclaWhitelistRequestsByCompanyAndProjectAndUserBadRequest().WithPayload(errorResponse(err)) } diff --git a/cla-backend-go/approval_list/helpers.go b/cla-backend-go/approval_list/helpers.go index fb53ce6d3..187c39bf7 100644 --- a/cla-backend-go/approval_list/helpers.go +++ b/cla-backend-go/approval_list/helpers.go @@ -8,7 +8,7 @@ import ( "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" "github.com/aws/aws-sdk-go/service/dynamodb/expression" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" log "github.com/communitybridge/easycla/cla-backend-go/logging" ) diff --git a/cla-backend-go/approval_list/models.go b/cla-backend-go/approval_list/models.go index 08f312b79..d1ba671ce 100644 --- a/cla-backend-go/approval_list/models.go +++ b/cla-backend-go/approval_list/models.go @@ -40,3 +40,15 @@ type CclaWhitelistRequest struct { DateModified string `dynamodbav:"date_modified"` Version string `dynamodbav:"version"` } + +// ApprovalItem data model + +type ApprovalItem struct { + ApprovalID string `dynamodbav:"approval_id"` + SignatureID string `dynamodbav:"signature_id"` + DateAdded string `dynamodbav:"date_added"` + DateCreated string `dynamodbav:"date_created"` + DateModified string `dynamodbav:"date_modified"` + ApprovalName string `dynamodbav:"approval_name"` + ApprovalCriteria string `dynamodbav:"approval_criteria"` +} diff --git a/cla-backend-go/approval_list/repository.go b/cla-backend-go/approval_list/repository.go index 5d9697f7c..72b6dbeab 100644 --- a/cla-backend-go/approval_list/repository.go +++ b/cla-backend-go/approval_list/repository.go @@ -7,7 +7,7 @@ import ( "errors" "fmt" - "github.com/communitybridge/easycla/cla-backend-go/project" + models2 "github.com/communitybridge/easycla/cla-backend-go/project/models" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" "github.com/aws/aws-sdk-go/service/dynamodb/expression" @@ -15,7 +15,7 @@ import ( "github.com/gofrs/uuid" "github.com/sirupsen/logrus" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/aws/aws-sdk-go/aws" @@ -33,15 +33,15 @@ const ( ProjectIDIndex = "ccla-approval-list-request-project-id-index" ) -// IRepository interface defines the functions for the whitelist service +// IRepository interface defines the functions for the approval list service type IRepository interface { - AddCclaWhitelistRequest(company *models.Company, project *models.ClaGroup, user *models.User, requesterName, requesterEmail string) (string, error) - GetCclaWhitelistRequest(requestID string) (*CLARequestModel, error) - ApproveCclaWhitelistRequest(requestID string) error - RejectCclaWhitelistRequest(requestID string) error - ListCclaWhitelistRequest(companyID string, projectID, status, userID *string) (*models.CclaWhitelistRequestList, error) + AddCclaApprovalRequest(company *models.Company, project *models.ClaGroup, user *models.User, requesterName, requesterEmail string) (string, error) + GetCclaApprovalListRequest(requestID string) (*CLARequestModel, error) + ApproveCclaApprovalListRequest(requestID string) error + RejectCclaApprovalListRequest(requestID string) error + ListCclaApprovalListRequests(companyID string, projectID, status, userID *string) (*models.CclaWhitelistRequestList, error) GetRequestsByCLAGroup(claGroupID string) ([]CLARequestModel, error) - UpdateRequestsByCLAGroup(model *project.DBProjectModel) error + UpdateRequestsByCLAGroup(model *models2.DBProjectModel) error } type repository struct { @@ -50,19 +50,19 @@ type repository struct { tableName string } -// NewRepository creates a new instance of the whitelist service +// NewRepository creates a new instance of the approval list service func NewRepository(awsSession *session.Session, stage string) IRepository { return repository{ stage: stage, dynamoDBClient: dynamodb.New(awsSession), - tableName: fmt.Sprintf("cla-%s-ccla-whitelist-requests", stage), + tableName: fmt.Sprintf("cla-%s-ccla-whitelist-requests", stage), // TODO: rename table } } -// AddCclaWhitelistRequest adds the specified request -func (repo repository) AddCclaWhitelistRequest(company *models.Company, project *models.ClaGroup, user *models.User, requesterName, requesterEmail string) (string, error) { +// AddCclaApprovalRequest adds the specified request +func (repo repository) AddCclaApprovalRequest(company *models.Company, project *models.ClaGroup, user *models.User, requesterName, requesterEmail string) (string, error) { f := logrus.Fields{ - "functionName": "AddCclaWhitelistRequest", + "functionName": "v1.approval_list.repository.AddCclaApprovalRequest", "requesterName": requesterName, "requesterEmail": requesterEmail, } @@ -96,24 +96,17 @@ func (repo repository) AddCclaWhitelistRequest(company *models.Company, project _, err = repo.dynamoDBClient.PutItem(input) if err != nil { - log.WithFields(f).Warnf("AddCclaWhitelistRequest - unable to create a new ccla approval request, error: %v", err) + log.WithFields(f).Warnf("AddCclaApprovalRequest - unable to create a new ccla approval request, error: %v", err) return status, err } - // Load the new record - should be able to find it quickly - record, readErr := repo.ListCclaWhitelistRequest(company.CompanyID, &project.ProjectID, nil, &user.UserID) - if readErr != nil || record == nil || record.List == nil { - log.WithFields(f).Warnf("AddCclaWhitelistRequest - unable to read newly created invite record, error: %v", readErr) - return status, err - } - - return record.List[0].RequestID, nil + return requestID.String(), nil } -// GetCclaWhitelistRequest fetches the specified request by ID -func (repo repository) GetCclaWhitelistRequest(requestID string) (*CLARequestModel, error) { +// GetCclaApprovalListRequest fetches the specified request by ID +func (repo repository) GetCclaApprovalListRequest(requestID string) (*CLARequestModel, error) { f := logrus.Fields{ - "functionName": "GetCclaWhitelistRequest", + "functionName": "v1.approval_list.repository.GetCclaApprovalListRequest", "requestID": requestID, } @@ -141,10 +134,10 @@ func (repo repository) GetCclaWhitelistRequest(requestID string) (*CLARequestMod return &requestModel, nil } -// ApproveCclaWhitelistRequest approves the specified request -func (repo repository) ApproveCclaWhitelistRequest(requestID string) error { +// ApproveCclaApprovalListRequest approves the specified request +func (repo repository) ApproveCclaApprovalListRequest(requestID string) error { f := logrus.Fields{ - "functionName": "ApproveCclaWhitelistRequest", + "functionName": "v1.approval_list.repository.ApproveCclaApprovalListRequest", "requestID": requestID, } @@ -180,10 +173,10 @@ func (repo repository) ApproveCclaWhitelistRequest(requestID string) error { return nil } -// RejectCclaWhitelistRequest rejects the specified request -func (repo repository) RejectCclaWhitelistRequest(requestID string) error { +// RejectCclaApprovalListRequest rejects the specified request +func (repo repository) RejectCclaApprovalListRequest(requestID string) error { f := logrus.Fields{ - "functionName": "RejectCclaWhitelistRequest", + "functionName": "v1.approval_list.repository.RejectCclaApprovalListRequest", "requestID": requestID, } @@ -220,13 +213,21 @@ func (repo repository) RejectCclaWhitelistRequest(requestID string) error { return nil } -// ListCclaWhitelistRequest list the requests for the specified query parameters -func (repo repository) ListCclaWhitelistRequest(companyID string, projectID, status, userID *string) (*models.CclaWhitelistRequestList, error) { +// ListCclaApprovalListRequests list the requests for the specified query parameters +func (repo repository) ListCclaApprovalListRequests(companyID string, projectID, status, userID *string) (*models.CclaWhitelistRequestList, error) { + f := logrus.Fields{ + "functionName": "v1.approval_list.repository.ListCclaApprovalListRequests", + "companyID": companyID, + "projectID": projectID, + "status": status, + "userID": utils.StringValue(userID), + } + if projectID == nil { - return nil, errors.New("project ID can not be nil for ListCclaWhitelistRequest") + return nil, errors.New("project ID can not be nil for ListCclaApprovalListRequests") } - log.Debugf("ListCclaWhitelistRequest with Company ID: %s, Project ID: %+v, Status: %+v, User ID: %+v", + log.WithFields(f).Debugf("ListCclaApprovalListRequests with Company ID: %s, Project ID: %+v, Status: %+v, User ID: %+v", companyID, projectID, status, userID) // hashkey is company_id, range key is project_id @@ -243,7 +244,7 @@ func (repo repository) ListCclaWhitelistRequest(companyID string, projectID, sta // Add the status filter if provided if status != nil { - log.Debugf("ListCclaWhitelistRequest - Adding status: %s", *status) + log.WithFields(f).Debugf("ListCclaApprovalListRequests - Adding status: %s", *status) statusFilterExpression := expression.Name("request_status").Equal(expression.Value(*status)) filter = addConditionToFilter(filter, statusFilterExpression, &filterAdded) } @@ -260,6 +261,7 @@ func (repo repository) ListCclaWhitelistRequest(companyID string, projectID, sta // Use the nice builder to create the expression expr, err := builder.Build() if err != nil { + log.WithFields(f).WithError(err).Warn("error building query") return nil, err } @@ -276,13 +278,13 @@ func (repo repository) ListCclaWhitelistRequest(companyID string, projectID, sta queryOutput, queryErr := repo.dynamoDBClient.Query(input) if queryErr != nil { - log.Warnf("list requests error while querying, error: %+v", queryErr) + log.WithFields(f).WithError(queryErr).Warnf("list requests error while querying, error: %+v", queryErr) return nil, queryErr } list, err := buildCclaWhitelistRequestsModels(queryOutput) if err != nil { - log.Warnf("unmarshall requests error while decoding the response, error: %+v", err) + log.WithFields(f).WithError(err).Warnf("unmarshall requests error while decoding the response, error: %+v", err) return nil, err } @@ -292,7 +294,7 @@ func (repo repository) ListCclaWhitelistRequest(companyID string, projectID, sta // GetRequestsByCLAGroup retrieves a list of requests for the specified CLA Group func (repo repository) GetRequestsByCLAGroup(claGroupID string) ([]CLARequestModel, error) { f := logrus.Fields{ - "functionName": "GetRequestsByCLAGroup", + "functionName": "v1.approval_list.repository.GetRequestsByCLAGroup", "claGroupID": claGroupID, "tableName": repo.tableName, "indexName": ProjectIDIndex, @@ -361,10 +363,10 @@ func (repo repository) GetRequestsByCLAGroup(claGroupID string) ([]CLARequestMod return projectRequests, nil } -// GetRequestsByCLAGroup retrieves a list of requests for the specified CLA Group -func (repo repository) UpdateRequestsByCLAGroup(model *project.DBProjectModel) error { +// UpdateRequestsByCLAGroup updates a list of requests for the specified CLA Group +func (repo repository) UpdateRequestsByCLAGroup(model *models2.DBProjectModel) error { f := logrus.Fields{ - "functionName": "UpdateRequestsByCLAGroup", + "functionName": "v1.approval_list.repository.UpdateRequestsByCLAGroup", "claGroupID": model.ProjectID, "tableName": repo.tableName, } diff --git a/cla-backend-go/approval_list/service.go b/cla-backend-go/approval_list/service.go index 77af8b8f8..277441893 100644 --- a/cla-backend-go/approval_list/service.go +++ b/cla-backend-go/approval_list/service.go @@ -9,16 +9,25 @@ import ( "fmt" "net/http" + repository2 "github.com/communitybridge/easycla/cla-backend-go/project/repository" + service2 "github.com/communitybridge/easycla/cla-backend-go/project/service" + + "github.com/sirupsen/logrus" + + "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" + + "github.com/communitybridge/easycla/cla-backend-go/emails" + "github.com/communitybridge/easycla/cla-backend-go/signatures" "github.com/communitybridge/easycla/cla-backend-go/utils" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/company" - "github.com/communitybridge/easycla/cla-backend-go/project" + "github.com/communitybridge/easycla/cla-backend-go/user" "github.com/communitybridge/easycla/cla-backend-go/users" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" ) // errors @@ -33,70 +42,96 @@ const ( // IService interface defines the service methods/functions type IService interface { - AddCclaWhitelistRequest(ctx context.Context, companyID string, claGroupID string, args models.CclaWhitelistRequestInput) (string, error) - ApproveCclaWhitelistRequest(ctx context.Context, companyID, claGroupID, requestID string) error - RejectCclaWhitelistRequest(ctx context.Context, companyID, claGroupID, requestID string) error - ListCclaWhitelistRequest(companyID string, claGroupID, status *string) (*models.CclaWhitelistRequestList, error) - ListCclaWhitelistRequestByCompanyProjectUser(companyID string, claGroupID, status, userID *string) (*models.CclaWhitelistRequestList, error) + AddCclaApprovalListRequest(ctx context.Context, companyID string, claGroupID string, args models.CclaWhitelistRequestInput) (string, error) + ApproveCclaApprovalListRequest(ctx context.Context, claUser *user.CLAUser, ClacompanyID, claGroupID, requestID string) error + RejectCclaApprovalListRequest(ctx context.Context, companyID, claGroupID, requestID string) error + ListCclaApprovalListRequest(companyID string, claGroupID, status *string) (*models.CclaWhitelistRequestList, error) + ListCclaApprovalListRequestByCompanyProjectUser(companyID string, claGroupID, status, userID *string) (*models.CclaWhitelistRequestList, error) } type service struct { - repo IRepository - userRepo users.UserRepository - companyRepo company.IRepository - projectRepo project.ProjectRepository - signatureRepo signatures.SignatureRepository - corpConsoleURL string - httpClient *http.Client + repo IRepository + projectService service2.Service + userRepo users.UserRepository + companyRepo company.IRepository + projectRepo repository2.ProjectRepository + signatureRepo signatures.SignatureRepository + projectsCLAGroupRepository projects_cla_groups.Repository + emailTemplateService emails.EmailTemplateService + corpConsoleURL string + httpClient *http.Client } -// NewService creates a new whitelist service -func NewService(repo IRepository, userRepo users.UserRepository, companyRepo company.IRepository, projectRepo project.ProjectRepository, signatureRepo signatures.SignatureRepository, corpConsoleURL string, httpClient *http.Client) IService { +// NewService creates a new approval list service +func NewService(repo IRepository, projectsCLAGroupRepository projects_cla_groups.Repository, projService service2.Service, userRepo users.UserRepository, companyRepo company.IRepository, projectRepo repository2.ProjectRepository, signatureRepo signatures.SignatureRepository, emailTemplateService emails.EmailTemplateService, corpConsoleURL string, httpClient *http.Client) IService { return service{ - repo: repo, - userRepo: userRepo, - companyRepo: companyRepo, - projectRepo: projectRepo, - signatureRepo: signatureRepo, - corpConsoleURL: corpConsoleURL, - httpClient: httpClient, + repo: repo, + projectService: projService, + userRepo: userRepo, + companyRepo: companyRepo, + projectRepo: projectRepo, + signatureRepo: signatureRepo, + projectsCLAGroupRepository: projectsCLAGroupRepository, + emailTemplateService: emailTemplateService, + corpConsoleURL: corpConsoleURL, + httpClient: httpClient, } } -func (s service) AddCclaWhitelistRequest(ctx context.Context, companyID string, claGroupID string, args models.CclaWhitelistRequestInput) (string, error) { - list, err := s.ListCclaWhitelistRequestByCompanyProjectUser(companyID, &claGroupID, nil, &args.ContributorID) +func (s service) AddCclaApprovalListRequest(ctx context.Context, companyID string, claGroupID string, args models.CclaWhitelistRequestInput) (string, error) { + f := logrus.Fields{ + "functionName": "v1.approval_list.service.AddCclaApprovalListRequest", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companyID": companyID, + "claGroupID": claGroupID, + "RecipientName": args.RecipientName, + "RecipientEmail": args.RecipientEmail, + "ContributorID": args.ContributorID, + "ContributorName": args.ContributorName, + "ContributorEmail": args.ContributorEmail, + "Message": args.Message, + } + + list, err := s.ListCclaApprovalListRequestByCompanyProjectUser(companyID, &claGroupID, nil, &args.ContributorID) if err != nil { - log.Warnf("AddCclaWhitelistRequest - error looking up existing contributor invite requests for company: %s, project: %s, user by id: %s with name: %s, email: %s, error: %+v", + log.WithFields(f).WithError(err).Warnf("error looking up existing contributor invite requests for company: %s, project: %s, user by id: %s with name: %s, email: %s, error: %+v", companyID, claGroupID, args.ContributorID, args.ContributorName, args.ContributorEmail, err) return "", err } for _, item := range list.List { if item.RequestStatus == "pending" || item.RequestStatus == "approved" { - log.Warnf("AddCclaWhitelistRequest - found existing contributor invite - id: %s, request for company: %s, project: %s, user by id: %s with name: %s, email: %s", + log.WithFields(f).Warnf("found existing contributor invite - id: %s, request for company: %s, project: %s, user by id: %s with name: %s, email: %s", list.List[0].RequestID, companyID, claGroupID, args.ContributorID, args.ContributorName, args.ContributorEmail) return "", ErrCclaApprovalRequestAlreadyExists } } companyModel, err := s.companyRepo.GetCompany(ctx, companyID) if err != nil { - log.Warnf("AddCclaWhitelistRequest - unable to lookup company by id: %s, error: %+v", companyID, err) + log.WithFields(f).Warnf("unable to lookup company by id: %s, error: %+v", companyID, err) return "", err } claGroupModel, err := s.projectRepo.GetCLAGroupByID(ctx, claGroupID, DontLoadRepoDetails) if err != nil { - log.Warnf("AddCclaWhitelistRequest - unable to lookup project by id: %s, error: %+v", claGroupID, err) + log.WithFields(f).Warnf("unable to lookup project by id: %s, error: %+v", claGroupID, err) return "", err } + + log.WithFields(f).Debugf("looking up user by user ID: %s", args.ContributorID) userModel, err := s.userRepo.GetUser(args.ContributorID) - if err != nil { - log.Warnf("AddCclaWhitelistRequest - unable to lookup user by id: %s with name: %s, email: %s, error: %+v", + if err != nil || userModel == nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup user by id: %s with name: %s, email: %s, error: %+v", args.ContributorID, args.ContributorName, args.ContributorEmail, err) - return "", err - } - if userModel == nil { - log.Warnf("AddCclaWhitelistRequest - unable to lookup user by id: %s with name: %s, email: %s, error: user object not found", - args.ContributorID, args.ContributorName, args.ContributorEmail) - return "", errors.New("invalid user") + + log.WithFields(f).Debugf("looking up user by user email: %s", args.ContributorEmail) + userModel, err = s.userRepo.GetUserByEmail(args.ContributorEmail) + if err != nil || userModel == nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup user by email: %s with name: %s, error: %+v", + args.ContributorName, args.ContributorEmail, err) + if err != nil { + return "", err + } + return "", errors.New("invalid user") + } } signed, approved := true, true @@ -104,14 +139,14 @@ func (s service) AddCclaWhitelistRequest(ctx context.Context, companyID string, pageSize := int64(5) sig, sigErr := s.signatureRepo.GetProjectCompanySignatures(ctx, companyID, claGroupID, &signed, &approved, nil, &sortOrder, &pageSize) if sigErr != nil || sig == nil || sig.Signatures == nil { - log.Warnf("AddCclaWhitelistRequest - unable to lookup signature by company id: %s project id: %s - (or no managers), sig: %+v, error: %+v", + log.WithFields(f).Warnf("unable to lookup signature by company id: %s project id: %s - (or no managers), sig: %+v, error: %+v", companyID, claGroupID, sig, err) return "", err } - requestID, addErr := s.repo.AddCclaWhitelistRequest(companyModel, claGroupModel, userModel, args.ContributorName, args.ContributorEmail) + requestID, addErr := s.repo.AddCclaApprovalRequest(companyModel, claGroupModel, userModel, args.ContributorName, args.ContributorEmail) if addErr != nil { - log.Warnf("AddCclaWhitelistRequest - unable to add Approval Request for id: %s with name: %s, email: %s, error: %+v", + log.WithFields(f).Warnf("unable to add Approval Request for id: %s with name: %s, email: %s, error: %+v", args.ContributorID, args.ContributorName, args.ContributorEmail, addErr) } @@ -121,67 +156,121 @@ func (s service) AddCclaWhitelistRequest(ctx context.Context, companyID string, return requestID, nil } -// ApproveCclaWhitelistRequest is the handler for the approve CLA request -func (s service) ApproveCclaWhitelistRequest(ctx context.Context, companyID, claGroupID, requestID string) error { - err := s.repo.ApproveCclaWhitelistRequest(requestID) +// ApproveCclaApprovalListRequest is the handler for the approve CLA request +func (s service) ApproveCclaApprovalListRequest(ctx context.Context, claUser *user.CLAUser, companyID, claGroupID, requestID string) error { + f := logrus.Fields{ + "functionName": "v1.approval_list.service.ApproveCclaApprovalListRequest", + "companyID": companyID, + "claGroupID": claGroupID, + "requestID": requestID, + "Approver": claUser.Name, + } + + err := s.repo.ApproveCclaApprovalListRequest(requestID) if err != nil { - log.Warnf("ApproveCclaWhitelistRequest - problem updating approved list with 'approved' status for request: %s, error: %+v", + log.WithFields(f).Warnf("ApproveCclaApprovalListRequest - problem updating approved list with 'approved' status for request: %s, error: %+v", requestID, err) return err } - requestModel, err := s.repo.GetCclaWhitelistRequest(requestID) + requestModel, err := s.repo.GetCclaApprovalListRequest(requestID) if err != nil { - log.Warnf("ApproveCclaWhitelistRequest - unable to lookup request by id: %s, error: %+v", requestID, err) + log.Warnf("ApproveCclaApprovalListRequest - unable to lookup request by id: %s, error: %+v", requestID, err) return err } companyModel, err := s.companyRepo.GetCompany(ctx, companyID) if err != nil { - log.Warnf("ApproveCclaWhitelistRequest - unable to lookup company by id: %s, error: %+v", companyID, err) + log.Warnf("ApproveCclaApprovalListRequest - unable to lookup company by id: %s, error: %+v", companyID, err) return err } - claGroupModel, err := s.projectRepo.GetCLAGroupByID(ctx, claGroupID, DontLoadRepoDetails) + _, err = s.projectRepo.GetCLAGroupByID(ctx, claGroupID, DontLoadRepoDetails) if err != nil { - log.Warnf("ApproveCclaWhitelistRequest - unable to lookup project by id: %s, error: %+v", claGroupID, err) + log.Warnf("ApproveCclaApprovalListRequest - unable to lookup project by id: %s, error: %+v", claGroupID, err) return err } if requestModel.UserEmails == nil { - msg := fmt.Sprintf("ApproveCclaWhitelistRequest - unable to send approval email - email missing for request: %+v, error: %+v", + msg := fmt.Sprintf("ApproveCclaApprovalListRequest - unable to send approval email - email missing for request: %+v, error: %+v", requestModel, err) log.Warnf(msg) return errors.New(msg) } + // Get project cla Group records + log.WithFields(f).Debugf("Getting SalesForce Projects for claGroup: %s ", claGroupID) + projectCLAGroups, getErr := s.projectsCLAGroupRepository.GetProjectsIdsForClaGroup(ctx, claGroupID) + if getErr != nil { + msg := fmt.Sprintf("Error getting SF projects for claGroup: %s ", claGroupID) + log.Debug(msg) + } + + if len(projectCLAGroups) == 0 { + msg := fmt.Sprintf("Error getting SF projects for claGroup: %s ", claGroupID) + return errors.New(msg) + } + + signedAtFoundation, signedErr := s.projectService.SignedAtFoundationLevel(ctx, projectCLAGroups[0].FoundationSFID) + if signedErr != nil { + msg := fmt.Sprintf("Problem checking project: %s , error: %+v", claGroupID, signedErr) + log.WithFields(f).Warn(msg) + return signedErr + } + + var projectSFIDs []string + foundationSFID := projectCLAGroups[0].FoundationSFID + + if signedAtFoundation { + // Get salesforce project by FoundationID + log.WithFields(f).Debugf("querying project service for project details...") + projectSFIDs = append(projectSFIDs, foundationSFID) + } else { + for _, pcg := range projectCLAGroups { + log.WithFields(f).Debugf("Getting salesforce project by SFID: %s ", pcg.ProjectSFID) + projectSFIDs = append(projectSFIDs, pcg.ProjectSFID) + } + } + // Send the email - sendRequestApprovedEmailToRecipient(companyModel, claGroupModel, requestModel.UserName, requestModel.UserEmails[0]) + s.sendRequestApprovedEmailToRecipient(ctx, + emails.CommonEmailParams{ + RecipientName: requestModel.UserName, + RecipientAddress: requestModel.UserEmails[0], + CompanyName: companyModel.CompanyName, + }, *claUser, projectSFIDs) return nil } -// RejectCclaWhitelistRequest is the handler for the decline CLA request -func (s service) RejectCclaWhitelistRequest(ctx context.Context, companyID, claGroupID, requestID string) error { - err := s.repo.RejectCclaWhitelistRequest(requestID) +// RejectCclaApprovalListRequest is the handler for the decline CLA request +func (s service) RejectCclaApprovalListRequest(ctx context.Context, companyID, claGroupID, requestID string) error { + f := logrus.Fields{ + "functionName": "v1.approval_list.service.RejectCclaApprovalListRequest", + "companyID": companyID, + "claGroupID": claGroupID, + "requestID": requestID, + } + + err := s.repo.RejectCclaApprovalListRequest(requestID) if err != nil { - log.Warnf("RejectCclaWhitelistRequest - problem updating approved list with 'rejected' status for request: %s, error: %+v", requestID, err) + log.WithFields(f).WithError(err).Warnf("problem updating approved list with 'rejected' status for request: %s, error: %+v", requestID, err) return err } - requestModel, err := s.repo.GetCclaWhitelistRequest(requestID) + requestModel, err := s.repo.GetCclaApprovalListRequest(requestID) if err != nil { - log.Warnf("RejectCclaWhitelistRequest - unable to lookup request by id: %s, error: %+v", requestID, err) + log.WithFields(f).WithError(err).Warnf("unable to lookup request by id: %s, error: %+v", requestID, err) return err } companyModel, err := s.companyRepo.GetCompany(ctx, companyID) if err != nil { - log.Warnf("RejectCclaWhitelistRequest - unable to lookup company by id: %s, error: %+v", companyID, err) + log.WithFields(f).WithError(err).Warnf("unable to lookup company by id: %s, error: %+v", companyID, err) return err } claGroupModel, err := s.projectRepo.GetCLAGroupByID(ctx, claGroupID, DontLoadRepoDetails) if err != nil { - log.Warnf("RejectCclaWhitelistRequest - unable to lookup project by id: %s, error: %+v", claGroupID, err) + log.WithFields(f).WithError(err).Warnf("unable to lookup project by id: %s, error: %+v", claGroupID, err) return err } @@ -190,32 +279,36 @@ func (s service) RejectCclaWhitelistRequest(ctx context.Context, companyID, claG pageSize := int64(5) sig, sigErr := s.signatureRepo.GetProjectCompanySignatures(ctx, companyID, claGroupID, &signed, &approved, nil, &sortOrder, &pageSize) if sigErr != nil || sig == nil || sig.Signatures == nil { - log.Warnf("RejectCclaWhitelistRequest - unable to lookup signature by company id: %s project id: %s - (or no managers), sig: %+v, error: %+v", + log.WithFields(f).WithError(sigErr).Warnf("unable to lookup signature by company id: %s project id: %s - (or no managers), sig: %+v, error: %+v", companyID, claGroupID, sig, err) return err } if requestModel.UserEmails == nil { - msg := fmt.Sprintf("RejectCclaWhitelistRequest - unable to send approval email - email missing for request: %+v, error: %+v", + msg := fmt.Sprintf("unable to send approval email - email missing for request: %+v, error: %+v", requestModel, err) - log.Warnf(msg) + log.WithFields(f).Warnf(msg) return errors.New(msg) } // Send the email - s.sendRequestRejectedEmailToRecipient(companyModel, claGroupModel, sig.Signatures[0], requestModel.UserName, requestModel.UserEmails[0]) + s.sendRequestRejectedEmailToRecipient(emails.CommonEmailParams{ + RecipientName: requestModel.UserName, + RecipientAddress: requestModel.UserEmails[0], + CompanyName: companyModel.CompanyName, + }, claGroupModel, sig.Signatures[0]) return nil } -// ListCclaWhitelistRequest is the handler for the list CLA request -func (s service) ListCclaWhitelistRequest(companyID string, claGroupID, status *string) (*models.CclaWhitelistRequestList, error) { - return s.repo.ListCclaWhitelistRequest(companyID, claGroupID, status, nil) +// ListCclaApprovalListRequest is the handler for the list CLA request +func (s service) ListCclaApprovalListRequest(companyID string, claGroupID, status *string) (*models.CclaWhitelistRequestList, error) { + return s.repo.ListCclaApprovalListRequests(companyID, claGroupID, status, nil) } -// ListCclaWhitelistRequestByCompanyProjectUser is the handler for the list CLA request -func (s service) ListCclaWhitelistRequestByCompanyProjectUser(companyID string, claGroupID, status, userID *string) (*models.CclaWhitelistRequestList, error) { - return s.repo.ListCclaWhitelistRequest(companyID, claGroupID, status, userID) +// ListCclaApprovalListRequestByCompanyProjectUser is the handler for the list CLA request +func (s service) ListCclaApprovalListRequestByCompanyProjectUser(companyID string, claGroupID, status, userID *string) (*models.CclaWhitelistRequestList, error) { + return s.repo.ListCclaApprovalListRequests(companyID, claGroupID, status, userID) } // sendRequestSentEmail sends emails to the CLA managers specified in the signature record @@ -225,7 +318,17 @@ func (s service) sendRequestSentEmail(companyModel *models.Company, claGroupMode // CLA Manager Name/Email from a list, send this to this recipient (CLA Manager) - otherwise we will send to all // CLA Managers on the Signature ACL if recipientName != "" && recipientEmail != "" { - s.sendRequestEmailToRecipient(companyModel, claGroupModel, contributorName, contributorEmail, recipientName, recipientEmail, message) + s.sendRequestEmailToRecipient(emails.RequestToAuthorizeTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: recipientName, + RecipientAddress: recipientEmail, + CompanyName: companyModel.CompanyName, + }, + ContributorName: contributorName, + ContributorEmail: contributorEmail, + OptionalMessage: message, + CompanyID: companyModel.CompanyID, + }, claGroupModel) return } @@ -235,7 +338,7 @@ func (s service) sendRequestSentEmail(companyModel *models.Company, claGroupMode // Need to determine which email... var whichEmail = "" if manager.LfEmail != "" { - whichEmail = manager.LfEmail + whichEmail = manager.LfEmail.String() } // If no LF Email try to grab the first other email in their email list @@ -246,47 +349,32 @@ func (s service) sendRequestSentEmail(companyModel *models.Company, claGroupMode log.Warnf("unable to send email to manager: %+v - no email on file...", manager) } else { // Send the email - s.sendRequestEmailToRecipient(companyModel, claGroupModel, contributorName, contributorEmail, manager.Username, whichEmail, message) + s.sendRequestEmailToRecipient(emails.RequestToAuthorizeTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: manager.Username, + RecipientAddress: whichEmail, + CompanyName: companyModel.CompanyName, + }, + ContributorName: contributorName, + ContributorEmail: contributorEmail, + OptionalMessage: message, + }, claGroupModel) } } } // sendRequestEmailToRecipient generates and sends an email to the specified recipient -func (s service) sendRequestEmailToRecipient(companyModel *models.Company, claGroupModel *models.ClaGroup, contributorName, contributorEmail, recipientName, recipientAddress, message string) { - companyName := companyModel.CompanyName +func (s service) sendRequestEmailToRecipient(emailParams emails.RequestToAuthorizeTemplateParams, claGroupModel *models.ClaGroup) { projectName := claGroupModel.ProjectName - - var optionalMessage = "" - if message != "" { - optionalMessage = fmt.Sprintf("
%s included the following message in the request:
", contributorName) - optionalMessage += fmt.Sprintf("%s
This is a notification email from EasyCLA regarding the project %s.
-%s (%s) has requested to be added to the Approved List as an authorized contributor from -%s to the project %s. You are receiving this message as a CLA Manager from %s for -%s.
-%s -If you want to add them to the Approved List, please -log into the EasyCLA Corporate -Console, where you can approve this user's request by selecting the 'Manage Approved List' and adding the -contributor's email, the contributor's entire email domain, their GitHub ID or the entire GitHub Organization for the -repository. This will permit them to begin contributing to %s on behalf of %s.
-If you are not certain whether to add them to the Approved List, please reach out to them directly to discuss.
-%s -%s`, - recipientName, projectName, contributorName, contributorEmail, - companyName, projectName, companyName, projectName, - optionalMessage, s.corpConsoleURL, - companyModel.CompanyID, projectName, companyName, - utils.GetEmailHelpContent(claGroupModel.Version == utils.V2), utils.GetEmailSignOffContent()) - - err := utils.SendEmail(subject, body, recipients) + subject := fmt.Sprintf("EasyCLA: Request to Authorize %s for %s", emailParams.ContributorName, projectName) + recipients := []string{emailParams.RecipientAddress} + body, err := emails.RenderRequestToAuthorizeTemplate(s.emailTemplateService, claGroupModel.Version, claGroupModel.ProjectExternalID, emailParams) + if err != nil { + log.Warnf("rendering email template : %s failed : %v", emails.RequestToAuthorizeTemplateName, err) + return + } + err = utils.SendEmail(subject, body, recipients) if err != nil { log.Warnf("problem sending email with subject: %s to recipients: %+v, error: %+v", subject, recipients, err) } else { @@ -295,20 +383,17 @@ repository. This will permit them to begin contributing to %s on behalf of %s. } // sendRequestRejectedEmailToRecipient generates and sends an email to the specified recipient -func (s service) sendRequestRejectedEmailToRecipient(companyModel *models.Company, claGroupModel *models.ClaGroup, signature *models.Signature, recipientName, recipientAddress string) { - companyName := companyModel.CompanyName +func (s service) sendRequestRejectedEmailToRecipient(emailParams emails.CommonEmailParams, claGroupModel *models.ClaGroup, signature *models.Signature) { projectName := claGroupModel.ProjectName - var claManagerText = "" - claManagerText += "Hello %s,
-This is a notification email from EasyCLA regarding the project %s.
-Your request to get added to the approval list from %s for %s was denied by one of the existing CLA Managers. -If you have further questions about this denial, please contact one of the existing CLA Managers from -%s for %s:
-%s -%s -%s`, - recipientName, projectName, - companyName, projectName, companyName, projectName, - claManagerText, - utils.GetEmailHelpContent(claGroupModel.Version == utils.V2), utils.GetEmailSignOffContent()) - - err := utils.SendEmail(subject, body, recipients) + recipients := []string{emailParams.RecipientAddress} + body, err := emails.RenderApprovalListRejectedTemplate( + s.emailTemplateService, claGroupModel.Version, claGroupModel.ProjectExternalID, emails.ApprovalListRejectedTemplateParams{ + CommonEmailParams: emailParams, + CLAManagers: emailCLAManagerParams, + }) + if err != nil { + log.Warnf("rendering email failed for : %s : %v", emails.ApprovalListRejectedTemplateName, err) + return + } + err = utils.SendEmail(subject, body, recipients) if err != nil { log.Warnf("problem sending email with subject: %s to recipients: %+v, error: %+v", subject, recipients, err) } else { @@ -348,40 +430,42 @@ If you have further questions about this denial, please contact one of the exist } } -func requestApprovedEmailToRecipientContent(companyModel *models.Company, claGroupModel *models.ClaGroup, recipientName, recipientAddress string) (string, string, []string) { - companyName := companyModel.CompanyName +func (s service) sendRequestApprovedEmailToRecipient(ctx context.Context, emailParams emails.CommonEmailParams, claUser user.CLAUser, projectSFIDs []string) { + f := logrus.Fields{ + "functionName": "v1.approval_list.service.sendRequestApprovedEmailToRecipient", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companyName": emailParams.CompanyName, + "recipientName": emailParams.RecipientName, + "recipientAddress": emailParams.RecipientAddress, + } + companyName := emailParams.CompanyName // subject string, body string, recipients []string subject := fmt.Sprintf("EasyCLA: Approved List Request Accepted for %s", companyName) - recipients := []string{recipientAddress} - body := fmt.Sprintf(` -Hello %s,
-This is a notification email from EasyCLA regarding the company %s.
-You have now been added to the approval list for %s.
-To get started, please navigate back to GitHub or Gerrit and start the authorization process. Once you select the -authorization link, you will be directed to the EasyCLA Contributor Console. GitHub users will need to authorize the -tool to see your GitHub user name and email. Gerrit users will first need to log in with their LF Account. On the -console landing page, select the corporate agreement option. To finish, search and select your company to acknowledge -your association with your company. This will complete the authorization process. For GitHub users, your pull request -will refresh and confirm that you are authorized. For Gerrit users, please log out of the UI and back in to complete the -authorization.
-%s -%s`, - recipientName, companyName, - companyName, - utils.GetEmailHelpContent(claGroupModel.Version == utils.V2), - utils.GetEmailSignOffContent()) - - return subject, body, recipients -} + recipients := []string{emailParams.RecipientAddress} + + approver := "" + if claUser.LFUsername != "" { + approver = claUser.LFUsername + } else if claUser.LFEmail != "" { + approver = claUser.LFEmail + } else if claUser.Emails != nil { + approver = claUser.Emails[0] + } -// sendRequestApprovedEmailToRecipient generates and sends an email to the specified recipient -func sendRequestApprovedEmailToRecipient(companyModel *models.Company, claGroupModel *models.ClaGroup, recipientName, recipientAddress string) { - subject, body, recipients := requestApprovedEmailToRecipientContent(companyModel, claGroupModel, recipientName, recipientAddress) - err := utils.SendEmail(subject, body, recipients) + body, err := emails.RenderApprovalListTemplate( + s.emailTemplateService, projectSFIDs, emails.ApprovalListApprovedTemplateParams{ + CommonEmailParams: emailParams, + Approver: approver, + }) if err != nil { - log.Warnf("problem sending email with subject: %s to recipients: %+v, error: %+v", subject, recipients, err) + log.WithFields(f).Warnf("rendering email failed for : %s : %v", emails.ApprovalListApprovedTemplateName, err) + return + } + err = utils.SendEmail(subject, body, recipients) + if err != nil { + log.WithFields(f).Warnf("problem sending email with subject: %s to recipients: %+v, error: %+v", subject, recipients, err) } else { - log.Debugf("sent email with subject: %s to recipients: %+v", subject, recipients) + log.WithFields(f).Debugf("sent email with subject: %s to recipients: %+v", subject, recipients) } } diff --git a/cla-backend-go/approval_list/service_test.go b/cla-backend-go/approval_list/service_test.go deleted file mode 100644 index ebc053a70..000000000 --- a/cla-backend-go/approval_list/service_test.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -package approval_list - -import ( - "testing" - - "github.com/communitybridge/easycla/cla-backend-go/gen/models" - "github.com/stretchr/testify/assert" -) - -func TestRequestApprovedEmailToRecipientContent(t *testing.T) { - subject, body, recipients := requestApprovedEmailToRecipientContent( - &models.Company{ - CompanyName: "gardenerLtd"}, - &models.ClaGroup{Version: "v2"}, - "john", - "john@john.com") - - assert.Equal(t, "EasyCLA: Approved List Request Accepted for gardenerLtd", subject) - assert.Equal(t, []string{"john@john.com"}, recipients) - assert.Contains(t, body, "Hello john,") - assert.Contains(t, body, "This is a notification email from EasyCLA regarding the company gardenerLtd") - assert.Contains(t, body, "You have now been added to the approval list for gardenerLtd") -} diff --git a/cla-backend-go/auth/auth0.go b/cla-backend-go/auth/auth0.go index 71d966aa2..c61627e44 100644 --- a/cla-backend-go/auth/auth0.go +++ b/cla-backend-go/auth/auth0.go @@ -9,7 +9,8 @@ import ( "net/http" "path" - "github.com/dgrijalva/jwt-go" + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/golang-jwt/jwt/v4" ) // Validator data model @@ -93,7 +94,12 @@ func (av Validator) getPemCert(token *jwt.Token) (interface{}, error) { if err != nil { return "", err } - defer resp.Body.Close() + defer func() { + closeErr := resp.Body.Close() + if closeErr != nil { + log.WithError(closeErr).Warn("problem closing response body") + } + }() var j = jwks{} err = json.NewDecoder(resp.Body).Decode(&j) diff --git a/cla-backend-go/auth/authorizer.go b/cla-backend-go/auth/authorizer.go index c973ed894..76532c094 100644 --- a/cla-backend-go/auth/authorizer.go +++ b/cla-backend-go/auth/authorizer.go @@ -5,6 +5,7 @@ package auth import ( "errors" + "fmt" "strings" "github.com/sirupsen/logrus" @@ -50,7 +51,7 @@ func NewAuthorizer(authValidator Validator, userPermissioner UserPermissioner) A // SecurityAuth creates a new CLA user based on the token and scopes func (a Authorizer) SecurityAuth(token string, scopes []string) (*user.CLAUser, error) { f := logrus.Fields{ - "functionName": "auth.SecurityAuth", + "functionName": "auth.authorizer.SecurityAuth", "scopes": strings.Join(scopes, ","), } // This handler is called by the runtime whenever a route needs authentication @@ -68,6 +69,7 @@ func (a Authorizer) SecurityAuth(token string, scopes []string) (*user.CLAUser, } return nil, err } + f["claims"] = fmt.Sprintf("%+v", claims) // Get the username from the token claims usernameClaim, ok := claims[a.authValidator.usernameClaim] @@ -123,6 +125,7 @@ func (a Authorizer) SecurityAuth(token string, scopes []string) (*user.CLAUser, } return nil, err } + //log.WithFields(f).Debugf("user loaded : %+v with scopes : %+v", lfuser, scopes) for _, scope := range scopes { switch Scope(scope) { @@ -149,5 +152,6 @@ func (a Authorizer) SecurityAuth(token string, scopes []string) (*user.CLAUser, } } + //log.WithFields(f).Debugf("returning user from auth : %+v", lfuser) return &lfuser, nil } diff --git a/cla-backend-go/cla_manager/handlers.go b/cla-backend-go/cla_manager/handlers.go index df311de9d..c08365d74 100644 --- a/cla-backend-go/cla_manager/handlers.go +++ b/cla-backend-go/cla_manager/handlers.go @@ -7,20 +7,30 @@ import ( "context" "fmt" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/cla_manager" + service2 "github.com/communitybridge/easycla/cla-backend-go/project/service" + + "github.com/go-openapi/strfmt" + + "github.com/LF-Engineering/lfx-kit/auth" + + user_service "github.com/communitybridge/easycla/cla-backend-go/v2/user-service" + "github.com/sirupsen/logrus" + + "github.com/communitybridge/easycla/cla-backend-go/emails" + + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/cla_manager" "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/aws/aws-sdk-go/aws" - sigAPI "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/signatures" + sigAPI "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/signatures" "github.com/communitybridge/easycla/cla-backend-go/signatures" "github.com/communitybridge/easycla/cla-backend-go/company" "github.com/communitybridge/easycla/cla-backend-go/events" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations" log "github.com/communitybridge/easycla/cla-backend-go/logging" - "github.com/communitybridge/easycla/cla-backend-go/project" "github.com/communitybridge/easycla/cla-backend-go/user" "github.com/communitybridge/easycla/cla-backend-go/users" "github.com/go-openapi/runtime/middleware" @@ -32,7 +42,7 @@ func isValidUser(claUser *user.CLAUser) bool { } // Configure is the API handler routine for the CLA manager routes -func Configure(api *operations.ClaAPI, service IService, companyService company.IService, projectService project.Service, usersService users.Service, sigService signatures.SignatureService, eventsService events.Service, corporateConsoleURL string) { // nolint +func Configure(api *operations.ClaAPI, service IService, companyService company.IService, projectService service2.Service, usersService users.Service, sigService signatures.SignatureService, eventsService events.Service, emailSvc emails.EmailTemplateService) { // nolint api.ClaManagerCreateCLAManagerRequestHandler = cla_manager.CreateCLAManagerRequestHandlerFunc(func(params cla_manager.CreateCLAManagerRequestParams, claUser *user.CLAUser) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint @@ -160,16 +170,16 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. } // Send an event - eventsService.LogEvent(&events.LogEventArgs{ - EventType: events.ClaManagerAccessRequestCreated, - ProjectID: params.ProjectID, - ClaGroupModel: claGroupModel, - CompanyID: params.CompanyID, - CompanyModel: companyModel, - LfUsername: params.Body.UserLFID, - UserID: params.Body.UserLFID, - UserModel: userModel, - ExternalProjectID: claGroupModel.ProjectExternalID, + eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ClaManagerAccessRequestCreated, + ProjectID: params.ProjectID, + ClaGroupModel: claGroupModel, + CompanyID: params.CompanyID, + CompanyModel: companyModel, + LfUsername: params.Body.UserLFID, + UserID: params.Body.UserLFID, + UserModel: userModel, + ProjectSFID: claGroupModel.ProjectExternalID, EventData: &events.CLAManagerRequestCreatedEventData{ RequestID: request.RequestID, CompanyName: companyModel.CompanyName, @@ -182,9 +192,15 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. // Send email to each manager for _, manager := range claManagers { - sendRequestAccessEmailToCLAManagers(companyModel, claGroupModel, - params.Body.UserName, params.Body.UserEmail, - manager.Username, manager.LfEmail) + sendRequestAccessEmailToCLAManagers(emailSvc, emails.RequestAccessToCLAManagersTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: manager.Username, + RecipientAddress: manager.LfEmail.String(), + CompanyName: companyModel.CompanyName, + }, + RequesterName: params.Body.UserName, + RequesterEmail: params.Body.UserEmail, + }, claGroupModel) } return cla_manager.NewCreateCLAManagerRequestOK().WithXRequestID(reqID).WithPayload(request) @@ -252,10 +268,18 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + f := logrus.Fields{ + "functionName": "cla_manager.handler.ClaManagerApproveCLAManagerRequestHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectID": params.ProjectID, + "CompanyID": params.CompanyID, + "RequestID": params.RequestID, + } + companyModel, companyErr := companyService.GetCompany(ctx, params.CompanyID) if companyErr != nil || companyModel == nil { msg := buildErrorMessageForApprove(params, companyErr) - log.Warn(msg) + log.WithFields(f).Warn(msg) return cla_manager.NewCreateCLAManagerRequestBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Message: msg, Code: "400", @@ -265,7 +289,7 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. claGroupModel, projectErr := projectService.GetCLAGroupByID(ctx, params.ProjectID) if projectErr != nil || claGroupModel == nil { msg := buildErrorMessageForApprove(params, projectErr) - log.Warn(msg) + log.WithFields(f).Warn(msg) return cla_manager.NewCreateCLAManagerRequestBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Message: msg, Code: "400", @@ -282,14 +306,14 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. }) if sigErr != nil || sigModels == nil { msg := buildErrorMessageForApprove(params, sigErr) - log.Warn(msg) + log.WithFields(f).Warn(msg) return cla_manager.NewApproveCLAManagerRequestBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Message: "CLA Manager Approve Request - error reading CCLA Signatures - " + msg, Code: "400", }) } if len(sigModels.Signatures) > 1 { - log.Warnf("returned multiple CCLA signature models for company ID: %s, project ID: %s for request ID: %s", + log.WithFields(f).Warnf("returned multiple CCLA signature models for company ID: %s, project ID: %s for request ID: %s", params.CompanyID, params.ProjectID, params.RequestID) } @@ -307,7 +331,7 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. request, err := service.ApproveRequest(params.CompanyID, params.ProjectID, params.RequestID) if err != nil { msg := buildErrorMessageForApprove(params, err) - log.Warn(msg) + log.WithFields(f).Warn(msg) return cla_manager.NewApproveCLAManagerRequestBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Message: msg, Code: "400", @@ -315,10 +339,10 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. } // Update the signature ACL - _, aclErr := sigService.AddCLAManager(ctx, sigModel.SignatureID.String(), request.UserID) + _, aclErr := sigService.AddCLAManager(ctx, sigModel.SignatureID, request.UserID) if aclErr != nil { msg := buildErrorMessageForApprove(params, aclErr) - log.Warn(msg) + log.WithFields(f).Warn(msg) return cla_manager.NewApproveCLAManagerRequestBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Message: msg, Code: "400", @@ -326,7 +350,7 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. } // Send an event - eventsService.LogEvent(&events.LogEventArgs{ + eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.ClaManagerAccessRequestApproved, ProjectID: params.ProjectID, CompanyID: params.CompanyID, @@ -344,12 +368,25 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. // Notify CLA Managers - send email to each manager for _, manager := range claManagers { - sendRequestApprovedEmailToCLAManagers(companyModel, claGroupModel, request.UserName, request.UserEmail, - manager.Username, manager.LfEmail) + sendRequestApprovedEmailToCLAManagers(emailSvc, emails.RequestApprovedToCLAManagersTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: manager.Username, + RecipientAddress: manager.LfEmail.String(), + CompanyName: companyModel.CompanyName, + }, + RequesterName: request.UserName, + RequesterEmail: request.UserEmail, + }, claGroupModel) } // Notify the requester - sendRequestApprovedEmailToRequester(companyModel, claGroupModel, request.UserName, request.UserEmail) + sendRequestApprovedEmailToRequester(emailSvc, emails.RequestApprovedToRequesterTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: request.UserName, + RecipientAddress: request.UserEmail, + CompanyName: companyModel.CompanyName, + }, + }, claGroupModel) return cla_manager.NewCreateCLAManagerRequestOK().WithXRequestID(reqID).WithPayload(request) }) @@ -358,11 +395,18 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. api.ClaManagerDenyCLAManagerRequestHandler = cla_manager.DenyCLAManagerRequestHandlerFunc(func(params cla_manager.DenyCLAManagerRequestParams, claUser *user.CLAUser) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + f := logrus.Fields{ + "functionName": "cla_manager.handler.ClaManagerDenyCLAManagerRequestHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectID": params.ProjectID, + "CompanyID": params.CompanyID, + "RequestID": params.RequestID, + } companyModel, companyErr := companyService.GetCompany(ctx, params.CompanyID) if companyErr != nil || companyModel == nil { msg := buildErrorMessageForDeny(params, companyErr) - log.Warn(msg) + log.WithFields(f).Warn(msg) return cla_manager.NewDenyCLAManagerRequestBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Message: msg, Code: "400", @@ -372,7 +416,7 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. claGroupModel, projectErr := projectService.GetCLAGroupByID(ctx, params.ProjectID) if projectErr != nil || claGroupModel == nil { msg := buildErrorMessageForDeny(params, projectErr) - log.Warn(msg) + log.WithFields(f).Warn(msg) return cla_manager.NewDenyCLAManagerRequestBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Message: msg, Code: "400", @@ -389,14 +433,14 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. }) if sigErr != nil || sigModels == nil { msg := buildErrorMessageForDeny(params, sigErr) - log.Warn(msg) + log.WithFields(f).Warn(msg) return cla_manager.NewApproveCLAManagerRequestBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Message: "CLA Manager Deny Request - error reading CCLA Signatures - " + msg, Code: "400", }) } if len(sigModels.Signatures) > 1 { - log.Warnf("returned multiple CCLA signature models for company ID: %s, project ID: %s for request ID: %s", + log.WithFields(f).Warnf("returned multiple CCLA signature models for company ID: %s, project ID: %s for request ID: %s", params.CompanyID, params.ProjectID, params.RequestID) } @@ -413,7 +457,7 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. request, err := service.DenyRequest(params.CompanyID, params.ProjectID, params.RequestID) if err != nil { msg := buildErrorMessageForDeny(params, err) - log.Warn(msg) + log.WithFields(f).Warn(msg) return cla_manager.NewDenyCLAManagerRequestBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Message: msg, Code: "400", @@ -421,7 +465,7 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. } // Send an event - eventsService.LogEvent(&events.LogEventArgs{ + eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.ClaManagerAccessRequestDenied, ProjectID: params.ProjectID, CompanyID: params.CompanyID, @@ -439,12 +483,23 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. // Notify CLA Managers - send email to each manager for _, manager := range claManagers { - sendRequestDeniedEmailToCLAManagers(companyModel, claGroupModel, request.UserName, request.UserEmail, - manager.Username, manager.LfEmail) + sendRequestDeniedEmailToCLAManagers(emailSvc, emails.RequestDeniedToCLAManagersTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: manager.Username, + RecipientAddress: manager.LfEmail.String(), + CompanyName: companyModel.CompanyName, + }, + RequesterName: request.UserName, + RequesterEmail: request.UserEmail, + }, claGroupModel) } // Notify the requester - sendRequestDeniedEmailToRequester(companyModel, claGroupModel, request.UserName, request.UserEmail) + sendRequestDeniedEmailToRequester(emailSvc, emails.CommonEmailParams{ + RecipientName: request.UserName, + RecipientAddress: request.UserEmail, + CompanyName: companyModel.CompanyName, + }, claGroupModel) return cla_manager.NewCreateCLAManagerRequestOK().WithPayload(request) }) @@ -453,6 +508,13 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. api.ClaManagerDeleteCLAManagerRequestHandler = cla_manager.DeleteCLAManagerRequestHandlerFunc(func(params cla_manager.DeleteCLAManagerRequestParams, claUser *user.CLAUser) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + f := logrus.Fields{ + "functionName": "cla_manager.handler.ClaManagerDeleteCLAManagerRequestHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectID": params.ProjectID, + "CompanyID": params.CompanyID, + "RequestID": params.RequestID, + } // Make sure the company id exists... companyModel, companyErr := companyService.GetCompany(ctx, params.CompanyID) @@ -468,7 +530,7 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. claGroupModel, projectErr := projectService.GetCLAGroupByID(ctx, params.ProjectID) if projectErr != nil || claGroupModel == nil { msg := buildErrorMessageForDelete(params, projectErr) - log.Warn(msg) + log.WithFields(f).Warn(msg) return cla_manager.NewDenyCLAManagerRequestBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Message: msg, Code: "400", @@ -479,7 +541,7 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. request, err := service.GetRequest(params.RequestID) if err != nil { msg := buildErrorMessageForDelete(params, err) - log.Warn(msg) + log.WithFields(f).Warn(msg) return cla_manager.NewDeleteCLAManagerRequestBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Message: msg, Code: "400", @@ -488,7 +550,7 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. if request == nil { msg := buildErrorMessageForDelete(params, err) - log.Warn(msg) + log.WithFields(f).Warn(msg) return cla_manager.NewDeleteCLAManagerRequestNotFound().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Message: msg, Code: "404", @@ -505,14 +567,16 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. }) if sigErr != nil || sigModels == nil { msg := buildErrorMessageForDelete(params, sigErr) - log.Warn(msg) - return cla_manager.NewDeleteCLAManagerRequestBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Message: "EasyCLA - 400 Bad Request - CLA Manager Delete Request - error reading CCLA Signatures - " + msg, - Code: "400", - }) + log.WithFields(f).Warn(msg) + return cla_manager.NewDeleteCLAManagerRequestBadRequest().WithXRequestID(reqID).WithPayload( + utils.ToV1ErrorResponse( + utils.ErrorResponseBadRequest( + reqID, + "CLA Manager Delete Request - error reading CCLA Signatures - "+msg, + ))) } if len(sigModels.Signatures) > 1 { - log.Warnf("returned multiple CCLA signature models for company ID: %s, project ID: %s for request ID: %s", + log.WithFields(f).Warnf("returned multiple CCLA signature models for company ID: %s, project ID: %s for request ID: %s", params.CompanyID, params.ProjectID, params.RequestID) } @@ -521,7 +585,7 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. if !currentUserInACL(claUser, claManagers) { msg := fmt.Sprintf("EasyCLA - 401 Unauthorized - CLA Manager %s / %s / %s not authorized to delete requests for company ID: %s, project ID: %s", claUser.UserID, claUser.Name, claUser.LFEmail, params.CompanyID, params.ProjectID) - log.Debug(msg) + log.WithFields(f).Debug(msg) return cla_manager.NewDeleteCLAManagerRequestUnauthorized().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Message: msg, Code: "401", @@ -532,7 +596,7 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. deleteErr := service.DeleteRequest(params.RequestID) if deleteErr != nil { msg := buildErrorMessageForDelete(params, deleteErr) - log.Warn(msg) + log.WithFields(f).Warn(msg) return cla_manager.NewDeleteCLAManagerRequestBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Message: msg, Code: "400", @@ -540,7 +604,7 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. } // Send an event - eventsService.LogEvent(&events.LogEventArgs{ + eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.ClaManagerAccessRequestDeleted, ProjectID: params.ProjectID, CompanyID: params.CompanyID, @@ -556,41 +620,72 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. }, }) - log.Debug("CLA Manager Delete - Returning Success") + log.WithFields(f).Debug("CLA Manager Delete - Returning Success") return cla_manager.NewDeleteCLAManagerRequestNoContent().WithXRequestID(reqID) }) api.ClaManagerAddCLAManagerHandler = cla_manager.AddCLAManagerHandlerFunc(func(params cla_manager.AddCLAManagerParams, claUser *user.CLAUser) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + f := logrus.Fields{ + "functionName": "cla_manager.handler.ClaManagerAddCLAManagerHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectID": params.ProjectID, + "CompanyID": params.CompanyID, + "UserLFID": params.Body.UserLFID, + "UserEmail": params.Body.UserEmail, + "UserName": params.Body.UserName, + } + log.WithFields(f).Debug("looking up user by user id...") userModel, userErr := usersService.GetUserByLFUserName(params.Body.UserLFID) if userErr != nil || userModel == nil { - msg := fmt.Sprintf("User lookup for user by LFID: %s failed ", params.Body.UserLFID) - log.Warn(msg) - return cla_manager.NewAddCLAManagerBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Message: "EasyCLA - 400 Bad Request - Add CLA Manager - error getting user - " + msg, - Code: "400", - }) + log.WithFields(f).Warnf("Add CLA Manager - user lookup by LFID: %s failed - attempting to lookup in SF...", params.Body.UserLFID) + userServiceClient := user_service.GetClient() + sfdcUserObject, userServiceLookupErr := userServiceClient.GetUserByUsername(params.Body.UserLFID) + if userServiceLookupErr != nil || sfdcUserObject == nil { + msg := fmt.Sprintf("Add CLA Manager - user lookup by LFID: %s failed ", params.Body.UserLFID) + log.WithFields(f).Warn(msg) + return cla_manager.NewAddCLAManagerBadRequest().WithXRequestID(reqID).WithPayload( + utils.ToV1ErrorResponse(utils.ErrorResponseBadRequestWithError(reqID, msg, userServiceLookupErr))) + } + + _, nowStr := utils.CurrentTime() + userModel, userErr = usersService.CreateUser(&models.User{ + Admin: false, + DateCreated: nowStr, + DateModified: nowStr, + Emails: userServiceClient.EmailsToSlice(sfdcUserObject), + GithubUsername: sfdcUserObject.GithubID, //this is the github username + LfEmail: strfmt.Email(userServiceClient.GetPrimaryEmail(sfdcUserObject)), + LfUsername: sfdcUserObject.Username, + Note: "created from SF record", + UserExternalID: sfdcUserObject.ID, + Username: sfdcUserObject.Username, + Version: "v1", + }, claUser) + if userErr != nil || userModel == nil { + msg := fmt.Sprintf("Add CLA Manager - user lookup by LFID: %s failed ", params.Body.UserLFID) + log.WithFields(f).Warn(msg) + return cla_manager.NewAddCLAManagerBadRequest().WithXRequestID(reqID).WithPayload( + utils.ToV1ErrorResponse(utils.ErrorResponseBadRequestWithError(reqID, msg, userErr))) + } } + companyModel, companyErr := companyService.GetCompany(ctx, params.CompanyID) if companyErr != nil || companyModel == nil { - msg := fmt.Sprintf("User lookup for company by ID: %s failed ", params.CompanyID) + msg := fmt.Sprintf("Add CLA Manager - error getting company by ID: %s failed ", params.CompanyID) log.Warn(msg) - return cla_manager.NewAddCLAManagerBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Message: "EasyCLA - 400 Bad Request - Add CLA Manager - error getting company - " + msg, - Code: "400", - }) + return cla_manager.NewAddCLAManagerBadRequest().WithXRequestID(reqID).WithPayload( + utils.ToV1ErrorResponse(utils.ErrorResponseBadRequest(reqID, msg))) } claGroupModel, projectErr := projectService.GetCLAGroupByID(ctx, params.ProjectID) if projectErr != nil || claGroupModel == nil { - msg := fmt.Sprintf("User lookup for project by ID: %s failed ", params.ProjectID) + msg := fmt.Sprintf("Add CLA Manager - error getting project - lookup for project by ID: %s failed ", params.ProjectID) log.Warn(msg) - return cla_manager.NewAddCLAManagerBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Message: "EasyCLA - 400 Bad Request - Add CLA Manager - error getting project - " + msg, - Code: "400", - }) + return cla_manager.NewAddCLAManagerBadRequest().WithXRequestID(reqID).WithPayload( + utils.ToV1ErrorResponse(utils.ErrorResponseBadRequest(reqID, msg))) } // Look up signature ACL to ensure the user is allowed to add CLA managers @@ -604,10 +699,8 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. if sigErr != nil || sigModels == nil { msg := buildErrorMessageAddManager("Add CLA Manager - signature lookup error", params, sigErr) log.Warn(msg) - return cla_manager.NewAddCLAManagerBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Message: "EasyCLA - 400 Bad Request - Add CLA Manager - error reading CCLA Signatures - " + msg, - Code: "400", - }) + return cla_manager.NewAddCLAManagerBadRequest().WithXRequestID(reqID).WithPayload( + utils.ToV1ErrorResponse(utils.ErrorResponseBadRequest(reqID, msg))) } if len(sigModels.Signatures) > 1 { log.Warnf("returned multiple CCLA signature models for company ID: %s, project ID: %s", @@ -620,21 +713,17 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. msg := fmt.Sprintf("EasyCLA - 401 Unauthorized - User %s / %s / %s is not authorized to add a CLA Manager for company ID: %s, project ID: %s", claUser.UserID, claUser.Name, claUser.LFEmail, params.CompanyID, params.ProjectID) log.Debug(msg) - return cla_manager.NewAddCLAManagerUnauthorized().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Message: msg, - Code: "401", - }) + return cla_manager.NewAddCLAManagerUnauthorized().WithXRequestID(reqID).WithPayload( + utils.ToV1ErrorResponse(utils.ErrorResponseUnauthorized(reqID, msg))) } // Audit Event sent from service upon success - signature, addErr := service.AddClaManager(ctx, params.CompanyID, params.ProjectID, params.Body.UserLFID) + signature, addErr := service.AddClaManager(ctx, ToAuthUser(claUser), params.CompanyID, params.ProjectID, params.Body.UserLFID, "") if addErr != nil { msg := buildErrorMessageAddManager("Add CLA Manager - Service Error", params, addErr) log.Warn(msg) - return cla_manager.NewAddCLAManagerBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Message: msg, - Code: "400", - }) + return cla_manager.NewAddCLAManagerBadRequest().WithXRequestID(reqID).WithPayload( + utils.ToV1ErrorResponse(utils.ErrorResponseBadRequest(reqID, msg))) } return cla_manager.NewAddCLAManagerOK().WithXRequestID(reqID).WithPayload(signature) @@ -644,16 +733,50 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. api.ClaManagerDeleteCLAManagerHandler = cla_manager.DeleteCLAManagerHandlerFunc(func(params cla_manager.DeleteCLAManagerParams, claUser *user.CLAUser) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + f := logrus.Fields{ + "functionName": "cla_manager.handler.ClaManagerDeleteCLAManagerHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectID": params.ProjectID, + "CompanyID": params.CompanyID, + "UserLFID": params.UserLFID, + } + + log.WithFields(f).Debug("looking up user by user id...") userModel, userErr := usersService.GetUserByLFUserName(params.UserLFID) if userErr != nil || userModel == nil { - msg := fmt.Sprintf("User lookup for user by LFID: %s failed ", params.UserLFID) - log.Warn(msg) - return cla_manager.NewDeleteCLAManagerBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Message: "EasyCLA - 400 Bad Request - Delete CLA Manager - error getting user - " + msg, - Code: "400", - }) + log.WithFields(f).Warnf("user lookup by LFID: %s failed - attempting to lookup in SF...", params.UserLFID) + userServiceClient := user_service.GetClient() + sfdcUserObject, userServiceLookupErr := userServiceClient.GetUserByUsername(params.UserLFID) + if userServiceLookupErr != nil || sfdcUserObject == nil { + msg := fmt.Sprintf("Delete CLA Manager - user lookup by LFID: %s failed ", params.UserLFID) + log.WithFields(f).Warn(msg) + return cla_manager.NewDeleteCLAManagerBadRequest().WithXRequestID(reqID).WithPayload( + utils.ToV1ErrorResponse(utils.ErrorResponseBadRequest(reqID, msg))) + } + + _, nowStr := utils.CurrentTime() + userModel, userErr = usersService.CreateUser(&models.User{ + Admin: false, + DateCreated: nowStr, + DateModified: nowStr, + Emails: userServiceClient.EmailsToSlice(sfdcUserObject), + GithubUsername: sfdcUserObject.GithubID, //this is the github username + LfEmail: strfmt.Email(userServiceClient.GetPrimaryEmail(sfdcUserObject)), + LfUsername: sfdcUserObject.Username, + Note: "created from SF record", + UserExternalID: sfdcUserObject.ID, + Username: sfdcUserObject.Username, + Version: "v1", + }, claUser) + if userErr != nil || userModel == nil { + msg := fmt.Sprintf("Add CLA Manager - user lookup by LFID: %s failed ", params.UserLFID) + log.WithFields(f).Warn(msg) + return cla_manager.NewDeleteCLAManagerBadRequest().WithXRequestID(reqID).WithPayload( + utils.ToV1ErrorResponse(utils.ErrorResponseBadRequestWithError(reqID, msg, userErr))) + } } + companyModel, companyErr := companyService.GetCompany(ctx, params.CompanyID) if companyErr != nil || companyModel == nil { msg := fmt.Sprintf("User lookup for company by ID: %s failed ", params.CompanyID) @@ -707,7 +830,7 @@ func Configure(api *operations.ClaAPI, service IService, companyService company. } // Audit Event sent from service upon success - signature, deleteErr := service.RemoveClaManager(ctx, params.CompanyID, params.ProjectID, params.UserLFID) + signature, deleteErr := service.RemoveClaManager(ctx, ToAuthUser(claUser), params.CompanyID, params.ProjectID, params.UserLFID, "") if deleteErr != nil { msg := buildErrorMessageDeleteManager("EasyCLA - 400 Bad Request - Delete CLA Manager - Service Error", params, deleteErr) @@ -794,34 +917,21 @@ func buildErrorMessageDeleteManager(errPrefix string, params cla_manager.DeleteC } // sendRequestAccessEmailToCLAManagers sends the request access email to the specified CLA Managers -func sendRequestAccessEmailToCLAManagers(companyModel *models.Company, claGroupModel *models.ClaGroup, requesterName, requesterEmail, recipientName, recipientAddress string) { - companyName := companyModel.CompanyName +func sendRequestAccessEmailToCLAManagers(emailSvc emails.EmailTemplateService, emailParams emails.RequestAccessToCLAManagersTemplateParams, claGroupModel *models.ClaGroup) { + companyName := emailParams.CompanyName projectName := claGroupModel.ProjectName // subject string, body string, recipients []string subject := fmt.Sprintf("EasyCLA: New CLA Manager Access Request for %s on %s", companyName, projectName) - recipients := []string{recipientAddress} - body := fmt.Sprintf(` -Hello %s,
-This is a notification email from EasyCLA regarding the project %s.
-You are currently listed as a CLA Manager from %s for the project %s. This means that you are able to maintain the -list of employees allowed to contribute to %s on behalf of your company, as well as view and manage the list of -your company’s CLA Managers for %s.
-%s (%s) has requested to be added as another CLA Manager from %s for %s. This would permit them to maintain the -lists of approved contributors and CLA Managers as well.
-If you want to permit this, please log into the EasyCLA Corporate Console, -select your company, then select the %s project. From the CLA Manager requests, you can approve this user as an -additional CLA Manager.
-%s -%s -`, - recipientName, projectName, - companyName, projectName, projectName, projectName, - requesterName, requesterEmail, companyName, projectName, - utils.GetCorporateURL(claGroupModel.Version == utils.V2), projectName, - utils.GetEmailHelpContent(claGroupModel.Version == utils.V2), utils.GetEmailSignOffContent()) - - err := utils.SendEmail(subject, body, recipients) + recipients := []string{emailParams.RecipientAddress} + body, err := emails.RenderRequestAccessToCLAManagersTemplate( + emailSvc, claGroupModel.Version, claGroupModel.ProjectExternalID, emailParams) + if err != nil { + log.Warnf("rendering email template : %s failed : %v", emails.RequestAccessToCLAManagersTemplateName, err) + return + } + + err = utils.SendEmail(subject, body, recipients) if err != nil { log.Warnf("problem sending email with subject: %s to recipients: %+v, error: %+v", subject, recipients, err) } else { @@ -829,30 +939,18 @@ additional CLA Manager. } } -func sendRequestApprovedEmailToCLAManagers(companyModel *models.Company, claGroupModel *models.ClaGroup, requesterName, requesterEmail, recipientName, recipientAddress string) { - companyName := companyModel.CompanyName +func sendRequestApprovedEmailToCLAManagers(emailSvc emails.EmailTemplateService, emailParams emails.RequestApprovedToCLAManagersTemplateParams, claGroupModel *models.ClaGroup) { projectName := claGroupModel.ProjectName // subject string, body string, recipients []string subject := fmt.Sprintf("EasyCLA: CLA Manager Access Approval Notice for %s", projectName) - recipients := []string{recipientAddress} - body := fmt.Sprintf(` -Hello %s,
-This is a notification email from EasyCLA regarding the project %s.
-The following user has been approved as a CLA Manager from %s for the project %s. This means that they can now -maintain the list of employees allowed to contribute to %s on behalf of your company, as well as view and manage the -list of company’s CLA Managers for %s.
-Hello %s,
-This is a notification email from EasyCLA regarding the project %s.
-You have now been approved as a CLA Manager from %s for the project %s. This means that you can now maintain the -list of employees allowed to contribute to %s on behalf of your company, as well as view and manage the list of your -company’s CLA Managers for %s.
-To get started, please log into the EasyCLA Corporate Console, and select your -company and then the project %s. From here you will be able to edit the list of approved employees and CLA Managers.
-%s -%s`, - requesterName, projectName, - companyName, projectName, projectName, projectName, - utils.GetCorporateURL(claGroupModel.Version == utils.V2), projectName, - utils.GetEmailHelpContent(claGroupModel.Version == utils.V2), utils.GetEmailSignOffContent()) - - err := utils.SendEmail(subject, body, recipients) + recipients := []string{emailParams.RecipientAddress} + body, err := emails.RenderRequestApprovedToRequesterTemplate(emailSvc, claGroupModel.Version, claGroupModel.ProjectExternalID, emailParams) + if err != nil { + log.Warnf("email template : %s failed rendering : %s", emails.RequestApprovedToRequesterTemplateName, err) + return + } + err = utils.SendEmail(subject, body, recipients) if err != nil { log.Warnf("problem sending email with subject: %s to recipients: %+v, error: %+v", subject, recipients, err) } else { @@ -890,29 +977,20 @@ company and then the project %s. From here you will be able to edit the list of } } -func sendRequestDeniedEmailToCLAManagers(companyModel *models.Company, claGroupModel *models.ClaGroup, requesterName, requesterEmail, recipientName, recipientAddress string) { - companyName := companyModel.CompanyName +func sendRequestDeniedEmailToCLAManagers(emailSvc emails.EmailTemplateService, emailParams emails.RequestDeniedToCLAManagersTemplateParams, claGroupModel *models.ClaGroup) { projectName := claGroupModel.ProjectName // subject string, body string, recipients []string subject := fmt.Sprintf("EasyCLA: CLA Manager Access Denied Notice for %s", projectName) - recipients := []string{recipientAddress} - body := fmt.Sprintf(` -Hello %s,
-This is a notification email from EasyCLA regarding the project %s.
-The following user has been denied as a CLA Manager from %s for the project %s. This means that they will not -be able to maintain the list of employees allowed to contribute to %s on behalf of your company.
-Hello %s,
-This is a notification email from EasyCLA regarding the project %s.
-You have been denied as a CLA Manager from %s for the project %s. This means that you can not maintain the -list of employees allowed to contribute to %s on behalf of your company.
-%s -%s`, - requesterName, projectName, - companyName, projectName, projectName, - utils.GetEmailHelpContent(claGroupModel.Version == utils.V2), utils.GetEmailSignOffContent()) - - err := utils.SendEmail(subject, body, recipients) + recipients := []string{emailParams.RecipientAddress} + body, err := emails.RenderRequestDeniedToRequesterTemplate(emailSvc, claGroupModel.Version, claGroupModel.ProjectExternalID, emails.RequestDeniedToRequesterTemplateParams{ + CommonEmailParams: emailParams, + }) + if err != nil { + log.Warnf("email template rendering %s failed : %v", emails.RequestDeniedToRequesterTemplateName, err) + return + } + + err = utils.SendEmail(subject, body, recipients) if err != nil { log.Warnf("problem sending email with subject: %s to recipients: %+v, error: %+v", subject, recipients, err) } else { log.Debugf("sent email with subject: %s to recipients: %+v", subject, recipients) } } + +// ToAuthUser converts a legacy v1 CLA user to a v2 platform auth user +func ToAuthUser(claUser *user.CLAUser) *auth.User { + return &auth.User{ + UserName: claUser.LFUsername, + Email: claUser.LFEmail, + ACL: auth.ACL{}, + } +} diff --git a/cla-backend-go/cla_manager/models.go b/cla-backend-go/cla_manager/models.go index ff5e3a756..7d9fad8e7 100644 --- a/cla-backend-go/cla_manager/models.go +++ b/cla-backend-go/cla_manager/models.go @@ -3,7 +3,7 @@ package cla_manager -import "github.com/communitybridge/easycla/cla-backend-go/gen/models" +import "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" // CLAManagerRequests data model type CLAManagerRequests struct { diff --git a/cla-backend-go/cla_manager/repository.go b/cla-backend-go/cla_manager/repository.go index 92fa3355d..f86d64838 100644 --- a/cla-backend-go/cla_manager/repository.go +++ b/cla-backend-go/cla_manager/repository.go @@ -6,7 +6,7 @@ package cla_manager import ( "fmt" - "github.com/communitybridge/easycla/cla-backend-go/project" + "github.com/communitybridge/easycla/cla-backend-go/project/models" "github.com/sirupsen/logrus" @@ -29,7 +29,7 @@ type IRepository interface { //nolint GetRequestsByUserID(companyID, projectID, userID string) (*CLAManagerRequests, error) GetRequest(requestID string) (*CLAManagerRequest, error) GetRequestsByCLAGroup(claGroupID string) ([]CLAManagerRequest, error) - UpdateRequestsByCLAGroup(model *project.DBProjectModel) error + UpdateRequestsByCLAGroup(model *models.DBProjectModel) error ApproveRequest(companyID, projectID, requestID string) (*CLAManagerRequest, error) DenyRequest(companyID, projectID, requestID string) (*CLAManagerRequest, error) @@ -480,7 +480,7 @@ func (repo repository) GetRequestsByCLAGroup(claGroupID string) ([]CLAManagerReq } // UpdateRequestsByCLAGroup handles updating the existing requests in our table based on the modified/updated CLA Group -func (repo repository) UpdateRequestsByCLAGroup(model *project.DBProjectModel) error { +func (repo repository) UpdateRequestsByCLAGroup(model *models.DBProjectModel) error { f := logrus.Fields{ "functionName": "UpdateRequestsByCLAGroup", "claGroupID": model.ProjectID, diff --git a/cla-backend-go/cla_manager/service.go b/cla-backend-go/cla_manager/service.go index fcfc355d1..00961bae2 100644 --- a/cla-backend-go/cla_manager/service.go +++ b/cla-backend-go/cla_manager/service.go @@ -7,13 +7,20 @@ import ( "context" "fmt" + service2 "github.com/communitybridge/easycla/cla-backend-go/project/service" + + "github.com/LF-Engineering/lfx-kit/auth" + "github.com/sirupsen/logrus" + + "github.com/communitybridge/easycla/cla-backend-go/emails" + "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" + "github.com/aws/aws-sdk-go/aws" "github.com/communitybridge/easycla/cla-backend-go/company" "github.com/communitybridge/easycla/cla-backend-go/events" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" - sigAPI "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/signatures" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + sigAPI "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/signatures" log "github.com/communitybridge/easycla/cla-backend-go/logging" - "github.com/communitybridge/easycla/cla-backend-go/project" "github.com/communitybridge/easycla/cla-backend-go/signatures" "github.com/communitybridge/easycla/cla-backend-go/users" "github.com/communitybridge/easycla/cla-backend-go/utils" @@ -32,30 +39,34 @@ type IService interface { PendingRequest(companyID, claGroupID, requestID string) (*models.ClaManagerRequest, error) DeleteRequest(requestID string) error - AddClaManager(ctx context.Context, companyID string, claGroupID string, LFID string) (*models.Signature, error) - RemoveClaManager(ctx context.Context, companyID string, claGroupID string, LFID string) (*models.Signature, error) + AddClaManager(ctx context.Context, authUser *auth.User, companyID string, claGroupID string, LFID string, projectSFName string) (*models.Signature, error) + RemoveClaManager(ctx context.Context, authUser *auth.User, companyID string, claGroupID string, LFID string, projectSFName string) (*models.Signature, error) } type service struct { - repo IRepository - companyService company.IService - projectService project.Service - usersService users.Service - sigService signatures.SignatureService - eventsService events.Service - corporateConsoleURL string + repo IRepository + projectClaRepository projects_cla_groups.Repository + companyService company.IService + projectService service2.Service + usersService users.Service + sigService signatures.SignatureService + eventsService events.Service + emailTemplateService emails.EmailTemplateService + corporateConsoleURL string } // NewService creates a new service object -func NewService(repo IRepository, companyService company.IService, projectService project.Service, usersService users.Service, sigService signatures.SignatureService, eventsService events.Service, corporateConsoleURL string) IService { +func NewService(repo IRepository, projectClaRepository projects_cla_groups.Repository, companyService company.IService, projectService service2.Service, usersService users.Service, sigService signatures.SignatureService, eventsService events.Service, emailTemplateService emails.EmailTemplateService, corporateConsoleURL string) IService { return service{ - repo: repo, - companyService: companyService, - projectService: projectService, - usersService: usersService, - sigService: sigService, - eventsService: eventsService, - corporateConsoleURL: corporateConsoleURL, + repo: repo, + projectClaRepository: projectClaRepository, + companyService: companyService, + projectService: projectService, + usersService: usersService, + sigService: sigService, + eventsService: eventsService, + emailTemplateService: emailTemplateService, + corporateConsoleURL: corporateConsoleURL, } } @@ -181,8 +192,17 @@ func (s service) DeleteRequest(requestID string) error { return nil } -// AddClaManager Adds LFID to Signature Access Control List list -func (s service) AddClaManager(ctx context.Context, companyID string, claGroupID string, LFID string) (*models.Signature, error) { +// AddClaManager Adds LFID to Signature Access Control list +func (s service) AddClaManager(ctx context.Context, authUser *auth.User, companyID string, claGroupID string, LFID string, projectSFName string) (*models.Signature, error) { + + f := logrus.Fields{ + "functionName": "v1.cla_manager.AddClaManager", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companyID": companyID, + "claGroupID": claGroupID, + "LFID": LFID, + "projectName": projectSFName, + } userModel, userErr := s.usersService.GetUserByLFUserName(LFID) if userErr != nil || userModel == nil { @@ -198,6 +218,11 @@ func (s service) AddClaManager(ctx context.Context, companyID string, claGroupID return nil, projectErr } + // if projectSFName is empty, we can set clagroup project name. + if projectSFName == "" { + projectSFName = claGroupModel.ProjectName + } + // Look up signature ACL to ensure the user can add cla manager signed := true @@ -209,11 +234,11 @@ func (s service) AddClaManager(ctx context.Context, companyID string, claGroupID claManagers := sigModel.SignatureACL - log.Debugf("Got Company signatures - Company: %s , Project: %s , signatureID: %s ", + log.WithFields(f).Debugf("Got Company signatures - Company: %s , Project: %s , signatureID: %s ", companyID, claGroupID, sigModel.SignatureID) // Update the signature ACL - addedSignature, aclErr := s.sigService.AddCLAManager(ctx, sigModel.SignatureID.String(), LFID) + addedSignature, aclErr := s.sigService.AddCLAManager(ctx, sigModel.SignatureID, LFID) if aclErr != nil { return nil, aclErr } @@ -221,34 +246,46 @@ func (s service) AddClaManager(ctx context.Context, companyID string, claGroupID // Update the company ACL record in EasyCLA companyACLError := s.companyService.AddUserToCompanyAccessList(ctx, companyID, LFID) if companyACLError != nil { - log.Warnf("AddCLAManager- Unable to add user to company ACL, companyID: %s, user: %s, error: %+v", companyID, LFID, companyACLError) + log.WithFields(f).Warnf("AddCLAManager- Unable to add user to company ACL, companyID: %s, user: %s, error: %+v", companyID, LFID, companyACLError) return nil, companyACLError } // Notify CLA Managers - send email to each manager for _, manager := range claManagers { - sendClaManagerAddedEmailToCLAManagers(companyModel, claGroupModel, userModel.Username, userModel.LfEmail, - manager.Username, manager.LfEmail) + sendClaManagerAddedEmailToCLAManagers(s.emailTemplateService, emails.ClaManagerAddedToCLAManagersTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: manager.Username, + RecipientAddress: manager.LfEmail.String(), + CompanyName: companyModel.CompanyName, + }, + Name: userModel.Username, + Email: userModel.LfEmail.String(), + }, claGroupModel) } // Notify the added user - sendClaManagerAddedEmailToUser(companyModel, claGroupModel, userModel.Username, userModel.LfEmail) + s.sendClaManagerAddedEmailToUser(s.emailTemplateService, emails.CommonEmailParams{ + RecipientName: userModel.Username, + RecipientAddress: userModel.LfEmail.String(), + CompanyName: companyModel.CompanyName, + }, claGroupModel) // Send an event - s.eventsService.LogEvent(&events.LogEventArgs{ - EventType: events.ClaManagerCreated, - ProjectID: claGroupID, - ClaGroupModel: claGroupModel, - CompanyID: companyID, - CompanyModel: companyModel, - LfUsername: LFID, - UserID: LFID, - UserModel: userModel, - ExternalProjectID: claGroupModel.ProjectExternalID, + s.eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ClaManagerCreated, + UserName: authUser.UserName, + LfUsername: authUser.UserName, + CLAGroupID: claGroupID, + CLAGroupName: claGroupModel.ProjectName, + ClaGroupModel: claGroupModel, + ProjectID: claGroupModel.ProjectExternalID, + ProjectSFID: claGroupModel.ProjectExternalID, + CompanyID: companyID, + CompanyModel: companyModel, EventData: &events.CLAManagerCreatedEventData{ CompanyName: companyModel.CompanyName, - ProjectName: claGroupModel.ProjectName, + ProjectName: projectSFName, UserName: userModel.Username, - UserEmail: userModel.LfEmail, + UserEmail: userModel.LfEmail.String(), UserLFID: userModel.LfUsername, }, }) @@ -280,7 +317,15 @@ func (s service) getCompanySignature(ctx context.Context, companyID string, claG } // RemoveClaManager removes lfid from signature acl with given company and project -func (s service) RemoveClaManager(ctx context.Context, companyID string, claGroupID string, LFID string) (*models.Signature, error) { +func (s service) RemoveClaManager(ctx context.Context, authUser *auth.User, companyID string, claGroupID string, LFID string, projectSFName string) (*models.Signature, error) { + + f := logrus.Fields{ + "functionName": "v1.cla_manager.RemoveClaManager", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupID, + "LFID": LFID, + "companyID": companyID, + } userModel, userErr := s.usersService.GetUserByLFUserName(LFID) if userErr != nil || userModel == nil { @@ -296,6 +341,11 @@ func (s service) RemoveClaManager(ctx context.Context, companyID string, claGrou return nil, projectErr } + // if projectSFName is empty, we can set clagroup project name. + if projectSFName == "" { + projectSFName = claGroupModel.ProjectName + } + signed := true approved := true sigModel, sigErr := s.sigService.GetProjectCompanySignature(ctx, companyID, claGroupID, &signed, &approved, nil, aws.Int64(5)) @@ -303,10 +353,17 @@ func (s service) RemoveClaManager(ctx context.Context, companyID string, claGrou return nil, sigErr } + if len(sigModel.SignatureACL) <= 1 { + // Can't delete the only remaining CLA Manager.... + return nil, &utils.CLAManagerError{ + Message: "unable to remove the only remaining CLA Manager - signed CLAs must have at least one CLA Manager", + } + } + // Update the signature ACL - updatedSignature, aclErr := s.sigService.RemoveCLAManager(ctx, sigModel.SignatureID.String(), LFID) + updatedSignature, aclErr := s.sigService.RemoveCLAManager(ctx, sigModel.SignatureID, LFID) if aclErr != nil || updatedSignature == nil { - log.Warnf("remove CLA Manager returned an error or empty signature model using Signature ID: %s, error: %+v", + log.WithFields(f).Warnf("remove CLA Manager returned an error or empty signature model using Signature ID: %s, error: %+v", sigModel.SignatureID, sigErr) return nil, aclErr } @@ -319,29 +376,41 @@ func (s service) RemoveClaManager(ctx context.Context, companyID string, claGrou claManagers := sigModel.SignatureACL // Notify CLA Managers - send email to each manager for _, manager := range claManagers { - sendClaManagerDeleteEmailToCLAManagers(companyModel, claGroupModel, userModel.LfUsername, userModel.LfEmail, - manager.Username, manager.LfEmail) + s.sendClaManagerDeleteEmailToCLAManagers(s.emailTemplateService, emails.ClaManagerDeletedToCLAManagersTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: manager.Username, + RecipientAddress: manager.LfEmail.String(), + CompanyName: companyModel.CompanyName, + }, + Name: userModel.LfUsername, + Email: userModel.LfEmail.String(), + }, claGroupModel) } // Notify the removed manager - sendRemovedClaManagerEmailToRecipient(companyModel, claGroupModel, userModel.LfUsername, userModel.LfEmail, claManagers) + sendRemovedClaManagerEmailToRecipient(s.emailTemplateService, emails.CommonEmailParams{ + RecipientName: userModel.LfUsername, + RecipientAddress: userModel.LfEmail.String(), + CompanyName: companyModel.CompanyName, + }, claGroupModel, claManagers) // Send an event s.eventsService.LogEvent(&events.LogEventArgs{ - EventType: events.ClaManagerDeleted, - ProjectID: claGroupID, - ClaGroupModel: claGroupModel, - CompanyID: companyID, - CompanyModel: companyModel, - LfUsername: userModel.LfUsername, - UserID: LFID, - UserModel: userModel, - ExternalProjectID: claGroupModel.ProjectExternalID, + EventType: events.ClaManagerDeleted, + LfUsername: authUser.UserName, + UserName: authUser.UserName, + CLAGroupID: claGroupID, + CLAGroupName: claGroupModel.ProjectName, + ClaGroupModel: claGroupModel, + ProjectID: claGroupModel.ProjectExternalID, + ProjectSFID: claGroupModel.ProjectExternalID, + CompanyID: companyID, + CompanyModel: companyModel, EventData: &events.CLAManagerDeletedEventData{ CompanyName: companyModel.CompanyName, - ProjectName: claGroupModel.ProjectName, + ProjectName: projectSFName, UserName: userModel.LfUsername, - UserEmail: userModel.LfEmail, + UserEmail: userModel.LfEmail.String(), UserLFID: LFID, }, }) @@ -349,29 +418,62 @@ func (s service) RemoveClaManager(ctx context.Context, companyID string, claGrou return updatedSignature, nil } -func sendClaManagerAddedEmailToUser(companyModel *models.Company, claGroupModel *models.ClaGroup, requesterName, requesterEmail string) { - companyName := companyModel.CompanyName - projectName := claGroupModel.ProjectName +type ProjectDetails struct { + ProjectName string + ProjectSFID string +} + +func (s service) getProjectDetails(ctx context.Context, claGroupModel *models.ClaGroup) ProjectDetails { + f := logrus.Fields{ + "functionName": "v1.cla_manager.getProjectDetails", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupModel.ProjectID, + } + projectDetails := ProjectDetails{ + ProjectName: claGroupModel.ProjectName, + ProjectSFID: claGroupModel.ProjectExternalID, + } + signedAtFoundation := false + + pcg, err := s.projectClaRepository.GetCLAGroup(ctx, claGroupModel.ProjectID) + if err != nil { + log.WithFields(f).Warnf("unable to fetch project cla group by project id: %s, error: %+v", claGroupModel.ProjectID, err) + } + + // check if cla group is signed at foundation level + if pcg != nil && pcg.FoundationSFID != "" { + signedAtFoundation, err = s.projectClaRepository.IsExistingFoundationLevelCLAGroup(ctx, pcg.FoundationSFID) + if err != nil { + log.WithFields(f).Warnf("unable to fetch foundation level cla group by foundation id: %s, error: %+v", pcg.FoundationSFID, err) + } + + if signedAtFoundation { + log.WithFields(f).Debugf("cla group is signed at foundation level...") + projectDetails.ProjectName = pcg.FoundationName + projectDetails.ProjectSFID = pcg.FoundationSFID + } + } + + return projectDetails +} + +func (s service) sendClaManagerAddedEmailToUser(emailSvc emails.EmailTemplateService, emailParams emails.CommonEmailParams, claGroupModel *models.ClaGroup) { + projectDetails := s.getProjectDetails(context.Background(), claGroupModel) + projectName := projectDetails.ProjectName + projectSFID := projectDetails.ProjectSFID // subject string, body string, recipients []string subject := fmt.Sprintf("EasyCLA: Added as CLA Manager for Project :%s", projectName) - recipients := []string{requesterEmail} - body := fmt.Sprintf(` -Hello %s,
-This is a notification email from EasyCLA regarding the project %s.
-You have been added as a CLA Manager from %s for the project %s. This means that you can now maintain the -list of employees allowed to contribute to %s on behalf of your company, as well as view and manage the list of your -company’s CLA Managers for %s.
-To get started, please log into the EasyCLA Corporate Console, and select your -company and then the project %s. From here you will be able to edit the list of approved employees and CLA Managers.
-%s -%s`, - requesterName, projectName, - companyName, projectName, projectName, projectName, - utils.GetCorporateURL(claGroupModel.Version == utils.V2), projectName, - utils.GetEmailHelpContent(claGroupModel.Version == utils.V2), utils.GetEmailSignOffContent()) - - err := utils.SendEmail(subject, body, recipients) + recipients := []string{emailParams.RecipientAddress} + body, err := emails.RenderClaManagerAddedEToUserTemplate(emailSvc, claGroupModel.Version, projectSFID, emails.ClaManagerAddedEToUserTemplateParams{ + CommonEmailParams: emailParams, + }) + if err != nil { + log.Warnf("email template render : %s failed : %v", emails.ClaManagerAddedEToUserTemplateName, err) + return + } + + err = utils.SendEmail(subject, body, recipients) if err != nil { log.Warnf("problem sending email with subject: %s to recipients: %+v, error: %+v", subject, recipients, err) } else { @@ -379,30 +481,19 @@ company and then the project %s. From here you will be able to edit the list of } } -func sendClaManagerAddedEmailToCLAManagers(companyModel *models.Company, claGroupModel *models.ClaGroup, name, email, recipientName, recipientAddress string) { - companyName := companyModel.CompanyName +func sendClaManagerAddedEmailToCLAManagers(emailSvc emails.EmailTemplateService, emailParams emails.ClaManagerAddedToCLAManagersTemplateParams, claGroupModel *models.ClaGroup) { projectName := claGroupModel.ProjectName // subject string, body string, recipients []string subject := fmt.Sprintf("EasyCLA: CLA Manager Added Notice for %s", projectName) - recipients := []string{recipientAddress} - body := fmt.Sprintf(` -Hello %s,
-This is a notification email from EasyCLA regarding the project %s.
-The following user has been added as a CLA Manager from %s for the project %s. This means that they can now -maintain the list of employees allowed to contribute to %s on behalf of your company, as well as view and manage the -list of company’s CLA Managers for %s.
-Hello %s,
-This is a notification email from EasyCLA regarding the project %s.
-You have been removed as a CLA Manager from %s for the project %s.
-If you have further questions about this, please contact one of the existing managers from -%s:
-%s -%s -%s`, - recipientName, projectName, companyName, projectName, companyName, companyManagerText, - utils.GetEmailHelpContent(claGroupModel.Version == utils.V2), utils.GetEmailSignOffContent()) - - err := utils.SendEmail(subject, body, recipients) + recipients := []string{emailParams.RecipientAddress} + body, err := emails.RenderRemovedCLAManagerTemplate( + emailSvc, + claGroupModel.Version, + claGroupModel.ProjectExternalID, + emails.RemovedCLAManagerTemplateParams{ + CommonEmailParams: emailParams, + CLAManagers: emailCLAManagerParams, + }) + + if err != nil { + log.Warnf("rendering the email content failed for : %s", emails.RemovedCLAManagerTemplateName) + return + } + + err = utils.SendEmail(subject, body, recipients) if err != nil { log.Warnf("problem sending email with subject: %s to recipients: %+v, error: %+v", subject, recipients, err) } else { @@ -479,24 +572,22 @@ func sendRemovedClaManagerEmailToRecipient(companyModel *models.Company, claGrou } } -func sendClaManagerDeleteEmailToCLAManagers(companyModel *models.Company, claGroupModel *models.ClaGroup, name, email, recipientName, recipientAddress string) { - companyName := companyModel.CompanyName - projectName := claGroupModel.ProjectName +func (s service) sendClaManagerDeleteEmailToCLAManagers(emailSvc emails.EmailTemplateService, emailParams emails.ClaManagerDeletedToCLAManagersTemplateParams, claGroupModel *models.ClaGroup) { + projectDetails := s.getProjectDetails(context.Background(), claGroupModel) + projectName := projectDetails.ProjectName + projectSFID := projectDetails.ProjectSFID // subject string, body string, recipients []string subject := fmt.Sprintf("EasyCLA: CLA Manager Removed Notice for %s", projectName) - recipients := []string{recipientAddress} - body := fmt.Sprintf(` -Hello %s,
-This is a notification email from EasyCLA regarding the project %s.
-%s(%s) has been removed as a CLA Manager from %s for the project %s.
-%s -%s -`, - recipientName, projectName, name, email, companyName, projectName, - utils.GetEmailHelpContent(claGroupModel.Version == utils.V2), utils.GetEmailSignOffContent()) - - err := utils.SendEmail(subject, body, recipients) + recipients := []string{emailParams.RecipientAddress} + body, err := emails.RenderClaManagerDeletedToCLAManagersTemplate(emailSvc, claGroupModel.Version, projectSFID, emailParams) + + if err != nil { + log.Warnf("email template render : %s failed : %v", emails.ClaManagerDeletedToCLAManagersTemplateName, err) + return + } + + err = utils.SendEmail(subject, body, recipients) if err != nil { log.Warnf("problem sending email with subject: %s to recipients: %+v, error: %+v", subject, recipients, err) } else { diff --git a/cla-backend-go/cmd/dynamo_events_lambda/main.go b/cla-backend-go/cmd/dynamo_events_lambda/main.go index f9dba32de..652f7e4e3 100644 --- a/cla-backend-go/cmd/dynamo_events_lambda/main.go +++ b/cla-backend-go/cmd/dynamo_events_lambda/main.go @@ -6,8 +6,19 @@ package main import ( "context" "encoding/json" + "fmt" "os" + "github.com/communitybridge/easycla/cla-backend-go/project/repository" + "github.com/communitybridge/easycla/cla-backend-go/project/service" + + v2Repositories "github.com/communitybridge/easycla/cla-backend-go/v2/repositories" + "github.com/communitybridge/easycla/cla-backend-go/v2/store" + + "github.com/communitybridge/easycla/cla-backend-go/v2/gitlab_organizations" + + gitlab "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" + "github.com/communitybridge/easycla/cla-backend-go/github_organizations" "github.com/communitybridge/easycla/cla-backend-go/utils" @@ -16,7 +27,6 @@ import ( "github.com/communitybridge/easycla/cla-backend-go/cla_manager" "github.com/communitybridge/easycla/cla-backend-go/gerrits" - "github.com/communitybridge/easycla/cla-backend-go/project" "github.com/communitybridge/easycla/cla-backend-go/repositories" acs_service "github.com/communitybridge/easycla/cla-backend-go/v2/acs-service" @@ -26,6 +36,7 @@ import ( "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" + "github.com/communitybridge/easycla/cla-backend-go/v2/approvals" "github.com/communitybridge/easycla/cla-backend-go/v2/dynamo_events" "github.com/communitybridge/easycla/cla-backend-go/token" @@ -80,44 +91,54 @@ func init() { usersRepo := users.NewRepository(awsSession, stage) userRepo := user.NewDynamoRepository(awsSession, stage) companyRepo := company.NewRepository(awsSession, stage) - signaturesRepo := signatures.NewRepository(awsSession, stage, companyRepo, usersRepo) projectClaGroupRepo := projects_cla_groups.NewRepository(awsSession, stage) + v2Repository := v2Repositories.NewRepository(awsSession, stage) repositoriesRepo := repositories.NewRepository(awsSession, stage) gerritRepo := gerrits.NewRepository(awsSession, stage) - projectRepo := project.NewRepository(awsSession, stage, repositoriesRepo, gerritRepo, projectClaGroupRepo) + projectRepo := repository.NewRepository(awsSession, stage, repositoriesRepo, gerritRepo, projectClaGroupRepo) eventsRepo := claevents.NewRepository(awsSession, stage) claManagerRequestsRepo := cla_manager.NewRepository(awsSession, stage) approvalListRequestsRepo := approval_list.NewRepository(awsSession, stage) githubOrganizationsRepo := github_organizations.NewRepository(awsSession, stage) + gitlabOrganizationRepo := gitlab_organizations.NewRepository(awsSession, stage) + storeRepo := store.NewRepository(awsSession, stage) + approvalsTableName := fmt.Sprintf("cla-%s-approvals", stage) + approvalRepo := approvals.NewRepository(stage, awsSession, approvalsTableName) token.Init(configFile.Auth0Platform.ClientID, configFile.Auth0Platform.ClientSecret, configFile.Auth0Platform.URL, configFile.Auth0Platform.Audience) - github.Init(configFile.Github.AppID, configFile.Github.AppPrivateKey, configFile.Github.AccessToken) + github.Init(configFile.GitHub.AppID, configFile.GitHub.AppPrivateKey, configFile.GitHub.AccessToken) + // initialize gitlab + gitlabApp := gitlab.Init(configFile.Gitlab.AppClientID, configFile.Gitlab.AppClientSecret, configFile.Gitlab.AppPrivateKey) user_service.InitClient(configFile.APIGatewayURL, configFile.AcsAPIKey) project_service.InitClient(configFile.APIGatewayURL) githubOrganizationsService := github_organizations.NewService(githubOrganizationsRepo, repositoriesRepo, projectClaGroupRepo) repositoriesService := repositories.NewService(repositoriesRepo, githubOrganizationsRepo, projectClaGroupRepo) - gerritService := gerrits.NewService(gerritRepo, &gerrits.LFGroup{ - LfBaseURL: configFile.LFGroup.ClientURL, - ClientID: configFile.LFGroup.ClientID, - ClientSecret: configFile.LFGroup.ClientSecret, - RefreshToken: configFile.LFGroup.RefreshToken, - }) + + gerritService := gerrits.NewService(gerritRepo) // Services - projectService := project.NewService(projectRepo, repositoriesRepo, gerritRepo, projectClaGroupRepo, usersRepo) + projectService := service.NewService(projectRepo, repositoriesRepo, gerritRepo, projectClaGroupRepo, usersRepo) type combinedRepo struct { users.UserRepository company.IRepository - project.ProjectRepository + repository.ProjectRepository + projects_cla_groups.Repository } + eventsService := claevents.NewService(eventsRepo, combinedRepo{ usersRepo, companyRepo, projectRepo, + projectClaGroupRepo, }) + usersService := users.NewService(usersRepo, eventsService) - companyService := company.NewService(companyRepo, configFile.CorporateConsoleURL, userRepo, usersService) + signaturesRepo := signatures.NewRepository(awsSession, stage, companyRepo, usersRepo, eventsService, repositoriesRepo, githubOrganizationsRepo, gerritService, approvalRepo) + v2RepositoryService := v2Repositories.NewService(repositoriesRepo, v2Repository, projectClaGroupRepo, githubOrganizationsRepo, gitlabOrganizationRepo, eventsService) + gitlabOrgService := gitlab_organizations.NewService(gitlabOrganizationRepo, v2RepositoryService, projectClaGroupRepo, storeRepo, usersService, signaturesRepo, companyRepo) + + companyService := company.NewService(companyRepo, configFile.CorporateConsoleV1URL, userRepo, usersService) v2CompanyService := v2Company.NewService(companyService, signaturesRepo, projectRepo, usersRepo, companyRepo, projectClaGroupRepo, eventsService) organization_service.InitClient(configFile.APIGatewayURL, eventsService) acs_service.InitClient(configFile.APIGatewayURL, configFile.AcsAPIKey) @@ -129,12 +150,17 @@ func init() { projectClaGroupRepo, eventsRepo, projectRepo, + gitlabOrganizationRepo, + v2Repository, projectService, githubOrganizationsService, repositoriesService, gerritService, claManagerRequestsRepo, - approvalListRequestsRepo) + approvalListRequestsRepo, + gitlabApp, + gitlabOrgService, + ) } func handler(ctx context.Context, event events.DynamoDBEvent) { diff --git a/cla-backend-go/cmd/functional_tests/cla_manager/cla_manager.go b/cla-backend-go/cmd/functional_tests/cla_manager/cla_manager.go index a7edc9b98..ec71d9dac 100644 --- a/cla-backend-go/cmd/functional_tests/cla_manager/cla_manager.go +++ b/cla-backend-go/cmd/functional_tests/cla_manager/cla_manager.go @@ -9,14 +9,14 @@ import ( "reflect" "github.com/communitybridge/easycla/cla-backend-go/cmd/functional_tests/test_models" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" "github.com/verdverm/frisby" ) var ( claManagerToken string claProspectiveManagerToken string - claManagerCreateRequestID string = "no-set" + claManagerCreateRequestID = "no-set" ) const ( diff --git a/cla-backend-go/cmd/functional_tests/health/health.go b/cla-backend-go/cmd/functional_tests/health/health.go index 8fe3d7ec7..e2937a344 100644 --- a/cla-backend-go/cmd/functional_tests/health/health.go +++ b/cla-backend-go/cmd/functional_tests/health/health.go @@ -9,7 +9,7 @@ import ( "reflect" "github.com/communitybridge/easycla/cla-backend-go/cmd/functional_tests/test_models" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" "github.com/verdverm/frisby" ) diff --git a/cla-backend-go/cmd/functional_tests/org_service/org_service.go b/cla-backend-go/cmd/functional_tests/org_service/org_service.go index d05685fe3..e241300f9 100644 --- a/cla-backend-go/cmd/functional_tests/org_service/org_service.go +++ b/cla-backend-go/cmd/functional_tests/org_service/org_service.go @@ -6,6 +6,8 @@ package org_service import ( "context" + "github.com/communitybridge/easycla/cla-backend-go/project/repository" + "github.com/communitybridge/easycla/cla-backend-go/cmd/functional_tests/test_models" "github.com/communitybridge/easycla/cla-backend-go/company" "github.com/communitybridge/easycla/cla-backend-go/config" @@ -13,7 +15,6 @@ import ( "github.com/communitybridge/easycla/cla-backend-go/gerrits" ini "github.com/communitybridge/easycla/cla-backend-go/init" log "github.com/communitybridge/easycla/cla-backend-go/logging" - "github.com/communitybridge/easycla/cla-backend-go/project" "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" "github.com/communitybridge/easycla/cla-backend-go/repositories" "github.com/communitybridge/easycla/cla-backend-go/users" @@ -56,21 +57,25 @@ func (t *TestBehaviour) RunIsUserHaveRoleScope() { type combinedRepo struct { users.UserRepository company.IRepository - project.ProjectRepository + repository.ProjectRepository + projects_cla_groups.Repository } + eventsRepo := events.NewRepository(awsSession, stage) usersRepo := users.NewRepository(awsSession, stage) companyRepo := company.NewRepository(awsSession, stage) repositoriesRepo := repositories.NewRepository(awsSession, stage) gerritRepo := gerrits.NewRepository(awsSession, stage) projectClaGroupRepo := projects_cla_groups.NewRepository(awsSession, stage) - projectRepo := project.NewRepository(awsSession, stage, repositoriesRepo, gerritRepo, projectClaGroupRepo) + projectRepo := repository.NewRepository(awsSession, stage, repositoriesRepo, gerritRepo, projectClaGroupRepo) eventsService := events.NewService(eventsRepo, combinedRepo{ usersRepo, companyRepo, projectRepo, + projectClaGroupRepo, }) + organization_service.InitClient(configFile.APIGatewayURL, eventsService) acs_service.InitClient(configFile.APIGatewayURL, configFile.AcsAPIKey) acsClient := acs_service.GetClient() diff --git a/cla-backend-go/cmd/functional_tests/signatures/signatures.go b/cla-backend-go/cmd/functional_tests/signatures/signatures.go index 340324efd..f0e423412 100644 --- a/cla-backend-go/cmd/functional_tests/signatures/signatures.go +++ b/cla-backend-go/cmd/functional_tests/signatures/signatures.go @@ -9,7 +9,7 @@ import ( "reflect" "github.com/communitybridge/easycla/cla-backend-go/cmd/functional_tests/test_models" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" "github.com/verdverm/frisby" ) diff --git a/cla-backend-go/cmd/generate_compound_attribute/main.go b/cla-backend-go/cmd/generate_compound_attribute/main.go new file mode 100644 index 000000000..5939d670a --- /dev/null +++ b/cla-backend-go/cmd/generate_compound_attribute/main.go @@ -0,0 +1,160 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "fmt" + "os" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" + "github.com/aws/aws-sdk-go/service/dynamodb/expression" + log "github.com/communitybridge/easycla/cla-backend-go/logging" +) + +var awsSession = session.Must(session.NewSession(&aws.Config{})) +var events EventInterface +var stage string + +const ( + tableName = "cla-%s-events" + attr1 = "event_company_sfid" + attr2 = "event_cla_group_id" + newAttr = "company_sfid_cla_group_id" + sep = "#" +) + +type EventInterface interface { + FetchAndUpdateDocuments(ctx context.Context) error + InsertCompoundAttribute(ctx context.Context, event EventModel) error +} + +type Config struct { + tableName string + dynamoDBClient *dynamodb.DynamoDB + stage string +} + +type EventModel struct { + EventID string `json:"event_id"` + EventCompanySFID string `json:"event_company_sfid"` + EventCLAGroupID string `json:"event_cla_group_id"` +} + +func (c Config) FetchAndUpdateDocuments(ctx context.Context) error { + builder := expression.NewBuilder() + filter := expression.Name(attr1).AttributeExists().And(expression.Name(attr2).AttributeExists()).And(expression.Name(newAttr).AttributeNotExists()) + builder = builder.WithFilter(filter) + expr, err := builder.Build() + if err != nil { + log.Error("stage not set", err) + return err + } + var lastEvaluatedKey string + // Assemble the query input parameters + scanInput := &dynamodb.ScanInput{ + TableName: aws.String(c.tableName), + FilterExpression: expr.Filter(), + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + } + var total int + for ok := true; ok; ok = lastEvaluatedKey != "" { + results, err := c.dynamoDBClient.ScanWithContext(ctx, scanInput) + if err != nil { + log.Error("Found error on ScanWithContext", err) + return err + } + log.Debugf("Found ---> %d Items in a batch", len(results.Items)) + if len(results.Items) > 0 { + total += len(results.Items) + + var events []EventModel + err = dynamodbattribute.UnmarshalListOfMaps(results.Items, &events) + if err != nil { + log.Error("Found error on UnmarshalListOfMaps", err) + return err + } + for _, event := range events { + err = c.InsertCompoundAttribute(ctx, event) + if err != nil { + log.Error("Found error on InsertCompoundAttribute", err) + return err + } + } + log.Debugf("All items are updated of the batch %d", len(results.Items)) + } + + if results.LastEvaluatedKey["event_id"] != nil { + //log.Debugf("LastEvaluatedKey: %+v", result.LastEvaluatedKey["signature_id"]) + lastEvaluatedKey = *results.LastEvaluatedKey["event_id"].S + scanInput.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{ + "event_id": { + S: aws.String(lastEvaluatedKey), + }, + } + } else { + lastEvaluatedKey = "" + } + } + fmt.Printf("total Items: %d", total) + return nil +} + +func (c Config) InsertCompoundAttribute(ctx context.Context, event EventModel) error { + updateExpression := expression.Set(expression.Name(newAttr), expression.Value(fmt.Sprintf("%s#%s", event.EventCompanySFID, event.EventCLAGroupID))) + expr, err := expression.NewBuilder().WithUpdate(updateExpression).Build() + if err != nil { + log.Error("Found error on NewBuilder", err) + return err + } + + _, err = c.dynamoDBClient.UpdateItemWithContext(ctx, &dynamodb.UpdateItemInput{ + TableName: aws.String(c.tableName), + Key: map[string]*dynamodb.AttributeValue{ + "event_id": { + S: aws.String(event.EventID), + }, + }, + UpdateExpression: expr.Update(), + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + }) + if err != nil { + log.Error("Found error on UpdateItemWithContext", err) + return err + } + log.Debugf("Updates event - %s", event.EventID) + return nil +} + +func NewRepository(awsSession *session.Session, stage string) EventInterface { + return &Config{ + dynamoDBClient: dynamodb.New(awsSession), + stage: stage, + tableName: fmt.Sprintf(tableName, stage), + } +} + +func init() { + stage = os.Getenv("STAGE") + if stage == "" { + log.Fatal("stage not set") + } + log.Infof("STAGE set to %s\n", stage) + events = NewRepository(awsSession, stage) +} + +func main() { + log.Debugf("Getting events that should be updated...") + + context := context.Background() + err := events.FetchAndUpdateDocuments(context) + if err != nil { + panic(err) + } +} diff --git a/cla-backend-go/cmd/gitlab/api/main.go b/cla-backend-go/cmd/gitlab/api/main.go new file mode 100644 index 000000000..dec0f60aa --- /dev/null +++ b/cla-backend-go/cmd/gitlab/api/main.go @@ -0,0 +1,117 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package main + +import ( + "flag" + "fmt" + "os" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/xanzy/go-gitlab" +) + +const ( + ProjectsURL = "https://gitlab.com/api/v4/projects" +) + +var state = flag.String("state", "failed", "the state of the MR to set") + +func main() { + flag.Parse() + + access_token := os.Getenv("GITLAB_ACCESS_TOKEN") + if access_token == "" { + log.Fatal("GITLAB_ACCESS_TOKEN is required") + } + + log.Infof("The gitlab access token is : %s", access_token) + + gitlabClient, err := gitlab.NewOAuthClient(access_token) + if err != nil { + log.Fatalf("creating client failed : %v", err) + } + + user, _, err := gitlabClient.Users.CurrentUser() + if err != nil { + log.Fatalf("fetching current user failed : %v", err) + } + + log.Infof("fetched current user : %s", user.Name) + + projects, _, err := gitlabClient.Projects.ListUserProjects(user.ID, &gitlab.ListProjectsOptions{}) + if err != nil { + log.Fatalf("listing projects failed : %v", err) + } + log.Printf("we fetched : %d projects for the account", len(projects)) + for _, p := range projects { + log.Println("Name : ", p.Name) + log.Println("ID: ", p.ID) + } + + projectID := 28118160 + commitSha := "f7036ab67a4e464e83e16af0b02d447c53fffa74" + + statuses, _, err := gitlabClient.Commits.GetCommitStatuses(projectID, commitSha, + &gitlab.GetCommitStatusesOptions{}) + if err != nil { + log.Fatalf("fetching commit statuses failed : %v", err) + } + + if len(statuses) == 0 { + log.Infof("no statuses found for commit sha") + setState := gitlab.Failed + if *state != string(gitlab.Failed) { + setState = gitlab.Success + } + + _, _, err = gitlabClient.Commits.SetCommitStatus(projectID, commitSha, &gitlab.SetCommitStatusOptions{ + State: setState, + Name: gitlab.String("easyCLA Bot"), + Description: gitlab.String(getDescription(setState)), + TargetURL: gitlab.String(getTargetURL("deniskurov@gmail.com")), + }) + if err != nil { + log.Fatalf("setting commit status for the sha failed : %v", err) + } + + statuses, _, err = gitlabClient.Commits.GetCommitStatuses(projectID, commitSha, + &gitlab.GetCommitStatusesOptions{}) + if err != nil { + log.Fatalf("fetching commit statuses failed : %v", err) + } + + } + + for _, status := range statuses { + log.Println("Status : ", status.Status) + if status.Status != *state { + log.Infof("setting state of commit sha to %s", *state) + _, _, err = gitlabClient.Commits.SetCommitStatus(projectID, commitSha, &gitlab.SetCommitStatusOptions{ + State: gitlab.BuildStateValue(*state), + Name: gitlab.String("easyCLA Bot"), + Description: gitlab.String(getDescription(gitlab.BuildStateValue(*state))), + TargetURL: gitlab.String(getTargetURL("deniskurov@gmail.com")), + }) + if err != nil { + log.Fatalf("setting commit status for the sha failed : %v", err) + } + } + log.Println("Status Name : ", status.Name) + log.Println("Status Description : ", status.Description) + log.Println("Status Author : ", status.Author.Name) + log.Println("Status Author Email : ", status.Author.Email) + } +} + +func getDescription(status gitlab.BuildStateValue) string { + if status == gitlab.Failed { + return "User hasn't signed CLA" + } + return "User signed CLA" +} + +func getTargetURL(email string) string { + return fmt.Sprintf("http://localhost:8080/gitlab/sign/%s", email) +} diff --git a/cla-backend-go/cmd/gitlab/auth/main.go b/cla-backend-go/cmd/gitlab/auth/main.go new file mode 100644 index 000000000..33c72e56d --- /dev/null +++ b/cla-backend-go/cmd/gitlab/auth/main.go @@ -0,0 +1,312 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package main + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "strconv" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/gin-gonic/gin" + "github.com/go-resty/resty/v2" + "github.com/xanzy/go-gitlab" +) + +const ( + clientRedirectURI = "http://localhost:8080/gitlab/oauth/callback" + clientAppID = "18718b478096e6a257eda51414d0d446ad28866c15187aa765f602fe906d0b17" + clientAppSecret = "8dd14ace0eb0e4674b849b6fed4ce51bbcc456fc62d9149aff15353c1dda6327" +) + +const ( + hookURL = "https://4c1ba3f4f3c1.ngrok.io/gitlab/events" +) + +type OauthSuccessResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + CreatedAt int `json:"created_at"` +} + +var passingUsers = map[string]bool{ + "deniskurov@gmail.com": true, +} + +func main() { + r := gin.Default() + r.GET("/gitlab/sign", func(c *gin.Context) { + email := c.Query("email") + if email == "" { + c.JSON(400, gin.H{ + "message": "email is required parameter", + }) + return + } + + projectID := c.Query("project_id") + if projectID == "" { + c.JSON(400, gin.H{ + "message": "projectID is required parameter", + }) + return + } + + lastCommitSha := c.Query("sha") + if lastCommitSha == "" { + c.JSON(400, gin.H{ + "message": "sha is required parameter", + }) + return + } + + projectIDInt, err := strconv.Atoi(projectID) + if err != nil { + log.Error("project id conversion failed ", err) + c.JSON(400, gin.H{ + "message": "project id conversion", + }) + return + } + + if err := setCommitStatus(projectIDInt, lastCommitSha, email, string(gitlab.Success)); err != nil { + log.Error("setting commit status failed", err) + c.JSON(500, gin.H{ + "message": "setting commit status failed", + }) + return + } + + log.Infof("email to sign is : %s", email) + log.Infof("project id : %s, sha : %s", projectID, lastCommitSha) + + c.JSON(http.StatusOK, gin.H{ + "message": fmt.Sprintf("user : %s, signed for project : %s", email, projectID), + }) + + }) + + r.POST("/gitlab/events", func(c *gin.Context) { + jsonData, err := ioutil.ReadAll(c.Request.Body) + event, err := gitlab.ParseWebhook(gitlab.EventTypeMergeRequest, jsonData) + if err != nil { + log.Error("parsing json body failed", err) + c.JSON(400, gin.H{ + "message": "code is required parameter", + }) + return + } + + mergeEvent, ok := event.(*gitlab.MergeEvent) + if !ok { + c.JSON(400, gin.H{ + "message": "type cast failed", + }) + return + } + + if mergeEvent.ObjectAttributes.State != "opened" { + c.JSON(200, gin.H{ + "message": "only interested in opened events", + }) + return + } + + projectName := mergeEvent.Project.Name + projectID := mergeEvent.Project.ID + + mergeID := mergeEvent.ObjectAttributes.IID + lastCommitSha := mergeEvent.ObjectAttributes.LastCommit.ID + lastCommitMessage := mergeEvent.ObjectAttributes.LastCommit.Message + + authorName := mergeEvent.ObjectAttributes.LastCommit.Author.Name + authorEmail := mergeEvent.ObjectAttributes.LastCommit.Author.Email + + log.Printf("Received MR (%d) for Project %s:%d", mergeID, projectName, projectID) + log.Printf("last commit : %s : %s", lastCommitSha, lastCommitMessage) + log.Printf("author name : %s, author email : %s", authorName, authorEmail) + + if err := setCommitStatus(projectID, lastCommitSha, authorEmail, ""); err != nil { + log.Error("setting commit status failed", err) + c.JSON(500, gin.H{ + "message": "setting commit status failed", + }) + return + } + + //empJSON, err := json.MarshalIndent(mergeEvent, "", " ") + //if err != nil { + // log.Fatalf(err.Error()) + //} + //fmt.Printf("MarshalIndent funnction output %s\n", string(empJSON)) + c.JSON(http.StatusOK, gin.H{}) + + }) + r.GET("/gitlab/oauth/callback", func(c *gin.Context) { + code := c.Query("code") + if code == "" { + c.JSON(400, gin.H{ + "message": "code is required parameter", + }) + return + } + + state := c.Query("state") + if state == "" { + c.JSON(400, gin.H{ + "message": "state is required parameter", + }) + return + } + log.Printf("received code : %s, STATE: %s", code, state) + + client := resty.New() + params := map[string]string{ + "client_id": clientAppID, + "client_secret": clientAppSecret, + "code": code, + "grant_type": "authorization_code", + "redirect_uri": clientRedirectURI, + } + + resp, err := client.R(). + SetQueryParams(params). + SetResult(&OauthSuccessResponse{}). + Post("https://gitlab.com/oauth/token") + + if err != nil { + c.JSON(500, gin.H{ + "message": fmt.Sprintf("getting the token failed : %v", err), + }) + return + } + + result := resp.Result().(*OauthSuccessResponse) + accessToken := result.AccessToken + + err = registerWebHooksForUserProjects(accessToken) + if err != nil { + log.Error("register webhook ", err) + } + + respData := gin.H{ + "message": "OK", + "data": result, + } + + if err != nil { + respData["error"] = err + } + + c.JSON(200, respData) + }) + r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080") +} + +func registerWebHooksForUserProjects(accessToken string) error { + gitlabClient, err := gitlab.NewOAuthClient(accessToken) + if err != nil { + return fmt.Errorf("creating client failed : %v", err) + } + + user, _, err := gitlabClient.Users.CurrentUser() + if err != nil { + return fmt.Errorf("fetching current user failed : %v", err) + } + + log.Infof("fetched current user : %s", user.Name) + + projects, _, err := gitlabClient.Projects.ListUserProjects(user.ID, &gitlab.ListProjectsOptions{}) + if err != nil { + return fmt.Errorf("listing projects failed : %v", err) + } + + log.Printf("we fetched : %d projects for the account", len(projects)) + + for _, p := range projects { + log.Println("**********************") + log.Println("Name : ", p.Name) + log.Println("ID: ", p.ID) + log.Infof("adding webhook to the project : %s (%d)", p.Name, p.ID) + if err := addCLAHookToProject(gitlabClient, p.ID); err != nil { + return fmt.Errorf("adding hook to the project : %s (%d) failed : %v", p.Name, p.ID, err) + } + } + + return nil +} + +func addCLAHookToProject(gitlabClient *gitlab.Client, projectID int) error { + _, _, err := gitlabClient.Projects.AddProjectHook(projectID, &gitlab.AddProjectHookOptions{ + URL: gitlab.String(hookURL), + MergeRequestsEvents: gitlab.Bool(true), + EnableSSLVerification: gitlab.Bool(false), + }) + return err +} + +func setCommitStatus(projectID interface{}, commitSha string, userEmail string, forceState string) error { + accessToken := os.Getenv("GITLAB_ACCESS_TOKEN") + if accessToken == "" { + return fmt.Errorf("GITLAB_ACCESS_TOKEN is required") + } + + gitlabClient, err := gitlab.NewOAuthClient(accessToken) + if err != nil { + return fmt.Errorf("creating client failed : %v", err) + } + + setState := gitlab.Failed + + if forceState == "" { + if passingUsers[userEmail] { + setState = gitlab.Success + } + } else { + setState = gitlab.BuildStateValue(forceState) + } + + options := &gitlab.SetCommitStatusOptions{ + State: setState, + Name: gitlab.String("easyCLA Bot"), + Description: gitlab.String(getDescription(setState)), + } + + if setState == gitlab.Failed { + options.TargetURL = gitlab.String(getTargetURL(projectID, commitSha, userEmail)) + } + + _, _, err = gitlabClient.Commits.SetCommitStatus(projectID, commitSha, options) + if err != nil { + return fmt.Errorf("setting commit status for the sha failed : %v", err) + } + + return nil +} + +func getDescription(status gitlab.BuildStateValue) string { + if status == gitlab.Failed { + return "User hasn't signed CLA" + } + return "User signed CLA" +} + +func getTargetURL(projectID interface{}, lastCommitSha, email string) string { + base := "http://localhost:8080/gitlab/sign" + + projectIDInt := projectID.(int) + projectIDStr := strconv.Itoa(projectIDInt) + + params := url.Values{} + params.Add("project_id", projectIDStr) + params.Add("sha", lastCommitSha) + params.Add("email", email) + + return base + "?" + params.Encode() +} diff --git a/cla-backend-go/cmd/gitlab/branch_protection/main.go b/cla-backend-go/cmd/gitlab/branch_protection/main.go new file mode 100644 index 000000000..0fd5edd7a --- /dev/null +++ b/cla-backend-go/cmd/gitlab/branch_protection/main.go @@ -0,0 +1,146 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package main + +import ( + "flag" + "fmt" + "os" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/xanzy/go-gitlab" +) + +const ( + possibleDefaultBranch = "main" +) + +var projectID = flag.Int("project", 0, "gitlab project id") + +func main() { + flag.Parse() + + if *projectID == 0 { + log.Fatalf("gitlab project id is missing") + } + + accessToken := os.Getenv("GITLAB_ACCESS_TOKEN") + if accessToken == "" { + log.Fatalf("GITLAB_ACCESS_TOKEN is required") + } + + gitlabClient, err := gitlab.NewOAuthClient(accessToken) + if err != nil { + log.Fatalf("creating client failed : %v", err) + } + + defaultBranch, err := getDefaultBranch(gitlabClient, *projectID) + if err != nil { + log.Fatalf("fetching the default branch failed : %v", err) + } + + log.Println("the default branch found is : ", defaultBranch.Name) + if err := setOrCreateProtection(gitlabClient, *projectID, defaultBranch.Name); err != nil { + log.Fatalf("setting branch protection for : %s failed : %v", defaultBranch.Name, err) + } + + log.Println("branch protection set for : ", defaultBranch.Name) +} + +func setOrCreateProtection(client *gitlab.Client, projectID int, protectionPattern string) error { + var err error + + protectedBranch, resp, err := client.ProtectedBranches.GetProtectedBranch(projectID, protectionPattern) + if err != nil && resp.StatusCode != 404 { + return fmt.Errorf("fetching existing branch failed : %v", err) + } + + if protectedBranch != nil { + if isProtectedBranchSet(protectedBranch) { + log.Println("branch protection already set, nothing to do") + return nil + } + //it's an existing one try to remove it first and re-create it + log.Println("removing old branch protection for string : ", protectionPattern) + _, err = client.ProtectedBranches.UnprotectRepositoryBranches(projectID, protectionPattern) + if err != nil { + return fmt.Errorf("removing protection for existing branch failed : %v", err) + } + } + + log.Println("re-creating branch protection for string ", protectionPattern) + if _, err = createBranchProtection(client, projectID, protectionPattern); err != nil { + return fmt.Errorf("recreating : %v", err) + } + return nil +} + +func createBranchProtection(client *gitlab.Client, projectID int, name string) (*gitlab.ProtectedBranch, error) { + protectedBranch, _, err := client.ProtectedBranches.ProtectRepositoryBranches(projectID, &gitlab.ProtectRepositoryBranchesOptions{ + Name: gitlab.String(name), + PushAccessLevel: gitlab.AccessLevel(gitlab.NoPermissions), + MergeAccessLevel: gitlab.AccessLevel(gitlab.MaintainerPermissions), + UnprotectAccessLevel: nil, + AllowForcePush: gitlab.Bool(false), + AllowedToPush: nil, + AllowedToMerge: nil, + AllowedToUnprotect: nil, + CodeOwnerApprovalRequired: nil, + }) + if err != nil { + return nil, fmt.Errorf("creating new branch protection failed : %v", err) + } + return protectedBranch, nil +} + +func isProtectedBranchSet(protectedBranch *gitlab.ProtectedBranch) bool { + //log.Println("checking branch protection for : ", spew.Sdump(protectedBranch)) + if protectedBranch.AllowForcePush { + return false + } + + if len(protectedBranch.PushAccessLevels) != 1 { + return false + } + + if protectedBranch.PushAccessLevels[0].AccessLevel != gitlab.NoPermissions { + return false + } + + if len(protectedBranch.MergeAccessLevels) != 1 { + return false + } + + if protectedBranch.MergeAccessLevels[0].AccessLevel != gitlab.MaintainerPermissions { + return false + } + + if len(protectedBranch.UnprotectAccessLevels) != 1 { + return false + } + + if protectedBranch.UnprotectAccessLevels[0].AccessLevel != gitlab.MaintainerPermissions { + return false + } + + return true +} + +// finds the default branch for the given project +func getDefaultBranch(client *gitlab.Client, projectID int) (*gitlab.Branch, error) { + project, _, err := client.Projects.GetProject(projectID, &gitlab.GetProjectOptions{}) + if err != nil { + return nil, fmt.Errorf("fetching project failed : %v", err) + } + + defaultBranch := project.DefaultBranch + + // first try with the possible option + branch, _, err := client.Branches.GetBranch(projectID, defaultBranch) + if err != nil { + return nil, fmt.Errorf("fetching default branch failed : %v", err) + } + + return branch, nil +} diff --git a/cla-backend-go/cmd/gitlab/gitlaborgevents/main.go b/cla-backend-go/cmd/gitlab/gitlaborgevents/main.go new file mode 100644 index 000000000..c082b5ac5 --- /dev/null +++ b/cla-backend-go/cmd/gitlab/gitlaborgevents/main.go @@ -0,0 +1,69 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package main + +import ( + "encoding/json" + "os" + + "github.com/aws/aws-lambda-go/events" + log "github.com/communitybridge/easycla/cla-backend-go/logging" +) + +// This function is useful for generating dynamodb events for GitlabOrg testing, the output of this function is used as +// ./bin/dynamo-events-lambda-mac {OUTPUT_OF_THIS_FUNCTION} +func main() { + // authInfo needed for creating the gitlab client + authInfo := os.Getenv("GITLAB_AUTH_INFO") + + event := events.DynamoDBEvent{ + Records: []events.DynamoDBEventRecord{ + { + EventSourceArn: "aws:dynamodb/cla-dev-gitlab-orgs", + EventName: "MODIFY", + EventSource: "aws:dynamodb", + Change: events.DynamoDBStreamRecord{ + OldImage: map[string]events.DynamoDBAttributeValue{ + "organization_id": events.NewStringAttribute("4ace6f9f-0518-4621-ae86-d0dacc75af83"), + "organization_full_path": events.NewStringAttribute("penguinsoft"), + "organization_sfid": events.NewStringAttribute("a092M00001If9uZQAR"), + "auto_enabled_cla_group_id": events.NewStringAttribute("40af3652-e8bf-489d-a917-cb2214a89640"), + "external_gitlab_group_id": events.NewNumberAttribute("12700028"), + "auth_state": events.NewStringAttribute("18eb90d1-8c36-4962-ba91-e264ccbcab3a"), + "organization_url": events.NewStringAttribute("https://gitlab.com/groups/penguinsoft"), + "auth_info": events.NewStringAttribute(authInfo), + "organization_name_lower": events.NewStringAttribute("penguinsoft"), + "project_sfid": events.NewStringAttribute("a092M00001If9uZQAR"), + "organization_name": events.NewStringAttribute("penguinsoft"), + "enabled": events.NewBooleanAttribute(false), + "auto_enabled": events.NewBooleanAttribute(false), + "branch_protection_enabled": events.NewBooleanAttribute(false), + }, + NewImage: map[string]events.DynamoDBAttributeValue{ + "organization_id": events.NewStringAttribute("4ace6f9f-0518-4621-ae86-d0dacc75af83"), + "organization_full_path": events.NewStringAttribute("penguinsoft"), + "organization_sfid": events.NewStringAttribute("a092M00001If9uZQAR"), + "auto_enabled_cla_group_id": events.NewStringAttribute("40af3652-e8bf-489d-a917-cb2214a89640"), + "external_gitlab_group_id": events.NewNumberAttribute("12700028"), + "auth_state": events.NewStringAttribute("18eb90d1-8c36-4962-ba91-e264ccbcab3a"), + "organization_url": events.NewStringAttribute("https://gitlab.com/groups/penguinsoft"), + "auth_info": events.NewStringAttribute(authInfo), + "organization_name_lower": events.NewStringAttribute("penguinsoft"), + "project_sfid": events.NewStringAttribute("a092M00001If9uZQAR"), + "organization_name": events.NewStringAttribute("penguinsoft"), + "enabled": events.NewBooleanAttribute(true), + "auto_enabled": events.NewBooleanAttribute(false), + "branch_protection_enabled": events.NewBooleanAttribute(true), + }, + }}, + }, + } + + b, err := json.Marshal(event) + if err != nil { + log.Fatalf("marshall : %v", err) + } + + log.Println(string(b)) +} diff --git a/cla-backend-go/cmd/gitlab/project_settings/main.go b/cla-backend-go/cmd/gitlab/project_settings/main.go new file mode 100644 index 000000000..15f44fd92 --- /dev/null +++ b/cla-backend-go/cmd/gitlab/project_settings/main.go @@ -0,0 +1,40 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "flag" + "os" + + gitlab_api "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/xanzy/go-gitlab" +) + +var projectID = flag.Int("project", 0, "gitlab project id") + +func main() { + flag.Parse() + + if *projectID == 0 { + log.Fatalf("gitlab project id is missing") + } + + accessToken := os.Getenv("GITLAB_ACCESS_TOKEN") + if accessToken == "" { + log.Fatalf("GITLAB_ACCESS_TOKEN is required") + } + + gitlabClient, err := gitlab.NewOAuthClient(accessToken) + if err != nil { + log.Fatalf("creating client failed : %v", err) + } + + if err := gitlab_api.EnableMergePipelineProtection(context.Background(), gitlabClient, *projectID); err != nil { + log.Fatalf("enabling merge pipeline protection failed : %v", err) + } + + log.Println("merge pipeline protection enabled successfully") +} diff --git a/cla-backend-go/cmd/gitlab/repoevents/main.go b/cla-backend-go/cmd/gitlab/repoevents/main.go new file mode 100644 index 000000000..fb4e4245c --- /dev/null +++ b/cla-backend-go/cmd/gitlab/repoevents/main.go @@ -0,0 +1,55 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package main + +import ( + "encoding/json" + + "github.com/aws/aws-lambda-go/events" + log "github.com/communitybridge/easycla/cla-backend-go/logging" +) + +// This function is useful for generating dynamodb events for Repository testing, the output of this function is used as +// ./bin/dynamo-events-lambda-mac {OUTPUT_OF_THIS_FUNCTION} +func main() { + event := events.DynamoDBEvent{ + Records: []events.DynamoDBEventRecord{ + { + EventSourceArn: "aws:dynamodb/cla-dev-repositories", + EventName: "MODIFY", + EventSource: "aws:dynamodb", + Change: events.DynamoDBStreamRecord{ + OldImage: map[string]events.DynamoDBAttributeValue{ + "repository_id": events.NewStringAttribute("1fa3de39-8274-4750-ba7c-242d5d659dd1"), + "repository_name": events.NewStringAttribute("easycla-gitlab-test"), + "repository_organization_name": events.NewStringAttribute("penguinsoft"), + "repository_project_id": events.NewStringAttribute("40af3652-e8bf-489d-a917-cb2214a89640"), + "repository_sfdc_id": events.NewStringAttribute("a092M00001If9uZQAR"), + "project_sfid": events.NewStringAttribute("a092M00001If9uZQAR"), + "repository_external_id": events.NewNumberAttribute("28893091"), + "repository_type": events.NewStringAttribute("gitlab"), + "enabled": events.NewBooleanAttribute(false), + }, + NewImage: map[string]events.DynamoDBAttributeValue{ + "repository_id": events.NewStringAttribute("1fa3de39-8274-4750-ba7c-242d5d659dd1"), + "repository_name": events.NewStringAttribute("easycla-gitlab-test"), + "repository_organization_name": events.NewStringAttribute("penguinsoft"), + "repository_project_id": events.NewStringAttribute("40af3652-e8bf-489d-a917-cb2214a89640"), + "repository_sfdc_id": events.NewStringAttribute("a092M00001If9uZQAR"), + "project_sfid": events.NewStringAttribute("a092M00001If9uZQAR"), + "repository_external_id": events.NewNumberAttribute("28893091"), + "repository_type": events.NewStringAttribute("gitlab"), + "enabled": events.NewBooleanAttribute(true), + }, + }}, + }, + } + + b, err := json.Marshal(event) + if err != nil { + log.Fatalf("marshall : %v", err) + } + + log.Println(string(b)) +} diff --git a/cla-backend-go/cmd/gitlab/webhook/main.go b/cla-backend-go/cmd/gitlab/webhook/main.go new file mode 100644 index 000000000..b7a187b98 --- /dev/null +++ b/cla-backend-go/cmd/gitlab/webhook/main.go @@ -0,0 +1,88 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package main + +import ( + "os" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/xanzy/go-gitlab" +) + +const ( + hookURL = "https://7e182f2774e2.ngrok.io/gitlab/events" +) + +func main() { + log.Println("register webhook") + access_token := os.Getenv("GITLAB_ACCESS_TOKEN") + if access_token == "" { + log.Fatal("GITLAB_ACCESS_TOKEN is required") + } + + log.Infof("The gitlab access token is : %s", access_token) + + gitlabClient, err := gitlab.NewOAuthClient(access_token) + if err != nil { + log.Fatalf("creating client failed : %v", err) + } + + user, _, err := gitlabClient.Users.CurrentUser() + if err != nil { + log.Fatalf("fetching current user failed : %v", err) + } + + log.Infof("fetched current user : %s", user.Name) + + projects, _, err := gitlabClient.Projects.ListUserProjects(user.ID, &gitlab.ListProjectsOptions{}) + if err != nil { + log.Fatalf("listing projects failed : %v", err) + } + log.Printf("we fetched : %d projects for the account", len(projects)) + for _, p := range projects { + log.Println("**********************") + log.Println("Name : ", p.Name) + log.Println("ID: ", p.ID) + hooks, _, err := gitlabClient.Projects.ListProjectHooks(p.ID, &gitlab.ListProjectHooksOptions{}) + + if err != nil { + log.Fatalf("fetching hooks for project : %s, failed : %v", p.Name, err) + } + + var claHookFound bool + for _, hook := range hooks { + log.Println("**********************") + log.Infof("hook ID : %d", hook.ID) + log.Infof("URL : %s", hook.URL) + log.Infof("Merge Request Events Enabled : %v", hook.MergeRequestsEvents) + log.Infof("Enable SSL Verification : %v", hook.EnableSSLVerification) + + if hookURL == hook.URL { + claHookFound = true + break + } + } + + if claHookFound { + log.Infof("CLA Hook was found nothing to do") + continue + } + + log.Infof("adding webhook to the project : %s (%d)", p.Name, p.ID) + if err := addCLAHookToProject(gitlabClient, p.ID); err != nil { + log.Fatalf("adding hook to the project : %s (%d) failed : %v", p.Name, p.ID, err) + } + + } + +} + +func addCLAHookToProject(gitlabClient *gitlab.Client, projectID int) error { + _, _, err := gitlabClient.Projects.AddProjectHook(projectID, &gitlab.AddProjectHookOptions{ + URL: gitlab.String(hookURL), + MergeRequestsEvents: gitlab.Bool(true), + EnableSSLVerification: gitlab.Bool(false), + }) + return err +} diff --git a/cla-backend-go/cmd/gitlab_repository_check/README.md b/cla-backend-go/cmd/gitlab_repository_check/README.md new file mode 100644 index 000000000..0133cc198 --- /dev/null +++ b/cla-backend-go/cmd/gitlab_repository_check/README.md @@ -0,0 +1,22 @@ +# GitLab Repository Check Lambda + +GitLab (currently) does not support sending callback/webhook events for GitLab project add or delete events. As a +result, we created a small lambda that runs periodically to check for any new GitLab project +(repository) add or deletes. + +The process/algorithm is: + +1. Query our database for registered GitLab Groups - filter by the enabled flag is true and where the Auto Enable flag + is true +1. For each GitLab group in our database... + 1. Create a new GitLab API client instance using the authorization token for the Git Group + 1. Query the GitLab API for the project list under the group (include sub-groups). This grabs the list of current + GitLab projects under the GitLab group. + 1. Query for GitLab project in DB matching this GitLab group path + 1. Identify deltas - this identifies how many new and deleted GitLap projects we need to process + 1. If any new GitLab projects, add to the DB, set enabled, create an event log + 1. If any removed/deleted GitLab projects, remove from the DB, create an event log + +## References + +- [GitLab Feature request discussion thread](https://gitlab.com/gitlab-com/marketing/community-relations/opensource-program/linux-foundation/-/issues/4#note_653255564) diff --git a/cla-backend-go/cmd/gitlab_repository_check/handler/handler.go b/cla-backend-go/cmd/gitlab_repository_check/handler/handler.go new file mode 100644 index 000000000..1097ffa52 --- /dev/null +++ b/cla-backend-go/cmd/gitlab_repository_check/handler/handler.go @@ -0,0 +1,367 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package handler + +import ( + "context" + "os" + "strconv" + + "github.com/communitybridge/easycla/cla-backend-go/signatures" + "github.com/communitybridge/easycla/cla-backend-go/v2/approvals" + "github.com/communitybridge/easycla/cla-backend-go/v2/common" + + "github.com/communitybridge/easycla/cla-backend-go/project/repository" + + "github.com/communitybridge/easycla/cla-backend-go/config" + + "github.com/aws/aws-sdk-go/aws/session" + + v1Company "github.com/communitybridge/easycla/cla-backend-go/company" + "github.com/communitybridge/easycla/cla-backend-go/events" + "github.com/communitybridge/easycla/cla-backend-go/gerrits" + "github.com/communitybridge/easycla/cla-backend-go/github_organizations" + gitLabApi "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" + gitlab "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" + ini "github.com/communitybridge/easycla/cla-backend-go/init" + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" + v1Repositories "github.com/communitybridge/easycla/cla-backend-go/repositories" + "github.com/communitybridge/easycla/cla-backend-go/users" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/communitybridge/easycla/cla-backend-go/v2/gitlab_organizations" + v2Repositories "github.com/communitybridge/easycla/cla-backend-go/v2/repositories" + "github.com/communitybridge/easycla/cla-backend-go/v2/store" + + "strings" + + "github.com/sirupsen/logrus" + goGitLab "github.com/xanzy/go-gitlab" +) + +var ( + awsSession *session.Session + stage string + configFile config.Config + gitLabApp *gitLabApi.App +) + +// Init initializes the handler +func Init() { + f := logrus.Fields{ + "functionName": "cmd.gitlab_repository_check.handler.Init", + } + ctx := utils.NewContext() + f[utils.XREQUESTID] = ctx.Value(utils.XREQUESTID) + log.WithFields(f).Debug("initializing...") + + // General initialization + ini.Init() + + var awsErr error + awsSession, awsErr = ini.GetAWSSession() + if awsErr != nil { + log.WithFields(f).WithError(awsErr).Panic("unable to load AWS session") + } + + // Need to initialize the system to load the configuration which contains a number of SSM parameters + stage = os.Getenv("STAGE") + if stage == "" { + log.WithFields(f).Panic("unable to determine STAGE - please set in the environment variable: 'STAGE' - expected one of [DEV, STAGING, PROD]") + } + + dynamodbRegion := os.Getenv("DYNAMODB_AWS_REGION") + if dynamodbRegion == "" { + log.WithFields(f).Panic("unable to determine DYNAMODB_AWS_REGION - please set in the environment variable: 'DYNAMODB_AWS_REGION'") + } + + var configErr error + configFile, configErr = config.LoadConfig("", awsSession, stage) + if configErr != nil { + log.WithFields(f).WithError(configErr).Panicf("Unable to load config - Error: %v", configErr) + } + + if configFile.Gitlab.AppClientID == "" { + log.WithFields(f).Panic("unable to determine configFile.Gitlab.AppClientID value - please set the configuration") + } + if configFile.Gitlab.AppClientSecret == "" { + log.WithFields(f).Panic("unable to determine configFile.Gitlab.AppClientSecret value - please set the configuration") + } + if configFile.Gitlab.AppPrivateKey == "" { + log.WithFields(f).Panic("unable to determine configFile.Gitlab.AppPrivateKey value - please set the configuration") + } + + // Create a new GitLab App client instance + gitLabApp = gitlab.Init(configFile.Gitlab.AppClientID, configFile.Gitlab.AppClientSecret, configFile.Gitlab.AppPrivateKey) + +} + +// Handler is invoked each time the lambda is triggered - https://docs.aws.amazon.com/lambda/latest/dg/golang-handler.html +func Handler(ctx context.Context) error { + f := logrus.Fields{ + "functionName": "cmd.update-project-statistics.Handler", + } + + // Add the x-request-id to the context + ctx = utils.NewContextFromParent(ctx) + f[utils.XREQUESTID] = ctx.Value(utils.XREQUESTID) + + // Repository Layer + usersRepo := users.NewRepository(awsSession, stage) + eventsRepo := events.NewRepository(awsSession, stage) + v1CompanyRepo := v1Company.NewRepository(awsSession, stage) + gerritRepo := gerrits.NewRepository(awsSession, stage) + v1ProjectClaGroupRepo := projects_cla_groups.NewRepository(awsSession, stage) + gitV1Repository := v1Repositories.NewRepository(awsSession, stage) + gitV2Repository := v2Repositories.NewRepository(awsSession, stage) + githubOrganizationsRepo := github_organizations.NewRepository(awsSession, stage) + gitlabOrganizationRepo := gitlab_organizations.NewRepository(awsSession, stage) + v1CLAGroupRepo := repository.NewRepository(awsSession, stage, gitV1Repository, gerritRepo, v1ProjectClaGroupRepo) + storeRepo := store.NewRepository(awsSession, stage) + + // Service Layer + + type combinedRepo struct { + users.UserRepository + v1Company.IRepository + repository.ProjectRepository + projects_cla_groups.Repository + } + + // Our service layer handlers + eventsService := events.NewService(eventsRepo, combinedRepo{ + usersRepo, + v1CompanyRepo, + v1CLAGroupRepo, + v1ProjectClaGroupRepo, + }) + + gerritService := gerrits.NewService(gerritRepo) + + approvalsTableName := "cla-" + stage + "-approvals" + + usersService := users.NewService(usersRepo, eventsService) + approvalsRepo := approvals.NewRepository(stage, awsSession, approvalsTableName) + signaturesRepo := signatures.NewRepository(awsSession, stage, v1CompanyRepo, usersRepo, eventsService, gitV1Repository, githubOrganizationsRepo, gerritService, approvalsRepo) + v2RepositoriesService := v2Repositories.NewService(gitV1Repository, gitV2Repository, v1ProjectClaGroupRepo, githubOrganizationsRepo, gitlabOrganizationRepo, eventsService) + // gitlabOrganizationsService := gitlab_organizations.NewService(gitlabOrganizationRepo, v2RepositoriesService, v1ProjectClaGroupRepo) + gitlabOrganizationService := gitlab_organizations.NewService(gitlabOrganizationRepo, v2RepositoriesService, v1ProjectClaGroupRepo, storeRepo, usersService, signaturesRepo, v1CompanyRepo) + + // Query GitLab Groups + // for each group + // if enabled and auto-enabled = true + // load token and client + // query for GitLab API repository list + // query for GitLab repositories in DB for this group path + // identify deltas + // if new, add to DB, create event log + // if deleted, remove from DB, create event log + + // gitLabGroups, err := gitlabOrganizationRepo.GetGitLabOrganizationsEnabled(ctx) + // if err != nil { + // log.WithFields(f).WithError(err).Warnf("problem querying for GitLab group/organizations that are enabled with auto-enabled flag set to true") + // return err + // } + gitLabGroups, err := gitlabOrganizationRepo.GetGitLabOrganizationsByProjectSFID(ctx, "a092h000004x5sGAAQ") + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem querying for GitLab group/organizations that are enabled with auto-enabled flag set to true") + return err + } + + log.WithFields(f).Debugf("start - checking %d GitLab projects for add/delete events", len(gitLabGroups.List)) + for _, gitLabGroup := range gitLabGroups.List { + claGroupID := gitLabGroup.AutoEnabledClaGroupID + log.WithFields(f).Debugf("start - processing GitLab group/organization: %s with group ID: %d associated with project SFID: %s", gitLabGroup.OrganizationURL, gitLabGroup.OrganizationExternalID, gitLabGroup.ProjectSfid) + + if claGroupID == "" { + log.WithFields(f).Debugf("GitLab group/organization: %s not fully onboarded - missing CLA Group ID", gitLabGroup.OrganizationURL) + pcg, err := v1ProjectClaGroupRepo.GetClaGroupIDForProject(ctx, gitLabGroup.ProjectSfid) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem querying for CLA Group ID for project SFID: %s", gitLabGroup.ProjectSfid) + continue + } + log.WithFields(f).Debug("found CLA Group ID: ", pcg.ClaGroupID) + claGroupID = pcg.ClaGroupID + } + + if gitLabGroup.AuthInfo == "" { + log.WithFields(f).Debugf("GitLab group/organization: %s not fully onboarded - missing authentication info - skipping", gitLabGroup.OrganizationURL) + continue + } + + oauthResponse, err := gitlabOrganizationService.RefreshGitLabOrganizationAuth(ctx, common.ToCommonModel(gitLabGroup)) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem refreshing GitLab group/organization: %s authentication info - skipping", gitLabGroup.OrganizationURL) + continue + } + + gitLabClient, gitLabClientErr := gitLabApi.NewGitlabOauthClient(*oauthResponse, gitLabApp) + if gitLabClientErr != nil { + log.WithFields(f).WithError(gitLabClientErr).Warnf("problem loading GitLab client for group/organization: %s - skipping", gitLabGroup.OrganizationURL) + continue + } + + gitLabProjects, getGitLabAPIError := gitLabApi.GetGroupProjectListByGroupID(ctx, gitLabClient, int(gitLabGroup.OrganizationExternalID)) + if getGitLabAPIError != nil { + log.WithFields(f).WithError(getGitLabAPIError).Warnf("problem loading GitLab projects for group/organization: %s using the groupID: %d - skipping GitLab Group/Organziation - skipping", gitLabGroup.OrganizationFullPath, gitLabGroup.OrganizationExternalID) + continue + } + log.WithFields(f).Debugf("found %d GitLab projects for group/organization: %s", len(gitLabProjects), gitLabGroup.OrganizationFullPath) + + gitLabDBProjects, getProjectListDBErr := gitV2Repository.GitLabGetRepositoriesByOrganizationName(ctx, gitLabGroup.OrganizationFullPath) + if getProjectListDBErr != nil { + if _, ok := getProjectListDBErr.(*utils.GitLabRepositoryNotFound); ok { + log.WithFields(f).Debugf("GitLab group/organization: %s does not have any repositories in the database", gitLabGroup.OrganizationFullPath) + } else { + log.WithFields(f).WithError(getProjectListDBErr).Warnf("problem loading GitLab projects for group/organization: %s from the database - skipping GitLab Group/Organziation - skipping", gitLabGroup.OrganizationFullPath) + continue + } + } + + newGitLabProjects := getNewProjects(gitLabProjects, gitLabDBProjects) + log.WithFields(f).Debugf("Found %d GitLab projects/repositories that are to be added for GitLab Group: %s", len(newGitLabProjects), gitLabGroup.OrganizationFullPath) + if len(newGitLabProjects) > 0 { + var gitLabProjectIDList []int64 + + // Build a quick list of the GitLab Project/repo ID values - the add repositories takes a list + for _, newGitLabProject := range newGitLabProjects { + gitLabProjectIDList = append(gitLabProjectIDList, int64(newGitLabProject.ID)) + } + + // Add the repositories - will generate a log event + _, addErr := v2RepositoriesService.GitLabAddRepositoriesWithEnabledFlag(ctx, gitLabGroup.ProjectSfid, &v2Repositories.GitLabAddRepoModel{ + ClaGroupID: claGroupID, + GroupName: gitLabGroup.OrganizationName, + ExternalID: gitLabGroup.OrganizationExternalID, + GroupFullPath: gitLabGroup.OrganizationFullPath, + ProjectIDList: gitLabProjectIDList, + }, true) // set to enabled when adding since this was added as a result of the auto-enable feature + if addErr != nil { + log.WithFields(f).WithError(addErr).Warnf("problem adding GitLab projects for group/organization: %s to the database", gitLabGroup.OrganizationFullPath) + } else { + log.WithFields(f).Debugf("added %d GitLab projects for group/organization: %s to the database", len(newGitLabProjects), gitLabGroup.OrganizationFullPath) + } + } + + gitLabProjects, getGitLabAPIError = gitLabApi.GetGroupProjectListByGroupID(ctx, gitLabClient, int(gitLabGroup.OrganizationExternalID)) + if getGitLabAPIError != nil { + log.WithFields(f).WithError(getGitLabAPIError).Warnf("problem loading GitLab projects for group/organization: %s using the groupID: %d - skipping GitLab Group/Organziation - skipping", gitLabGroup.OrganizationFullPath, gitLabGroup.OrganizationExternalID) + continue + } + log.WithFields(f).Debugf("found %d GitLab projects for group/organization: %s", len(gitLabProjects), gitLabGroup.OrganizationFullPath) + + dBProjects, getProjectListDBErr := gitV2Repository.GitLabGetRepositoriesByOrganizationName(ctx, gitLabGroup.OrganizationFullPath) + if getProjectListDBErr != nil { + if _, ok := getProjectListDBErr.(*utils.GitLabRepositoryNotFound); ok { + log.WithFields(f).Debugf("GitLab group/organization: %s does not have any repositories in the database", gitLabGroup.OrganizationFullPath) + } else { + log.WithFields(f).WithError(getProjectListDBErr).Warnf("problem loading GitLab projects for group/organization: %s from the database - skipping GitLab Group/Organziation - skipping", gitLabGroup.OrganizationFullPath) + continue + } + } + log.WithFields(f).Debugf("Found %d GitLab projects/repositories for GitLab Group: %s", len(dBProjects), gitLabGroup.OrganizationFullPath) + + deletedGitLabProjects := getDeletedProjects(gitLabProjects, dBProjects) + log.WithFields(f).Debugf("Found %d GitLab projects/repositories that are to be removed from the GitLab Group: %s", len(deletedGitLabProjects), gitLabGroup.OrganizationFullPath) + if len(deletedGitLabProjects) > 0 { + for _, gitLabProjectDBRecord := range deletedGitLabProjects { + repositoryExternalID, parseIntErr := strconv.ParseInt(gitLabProjectDBRecord.RepositoryExternalID, 10, 64) + if parseIntErr != nil { + log.WithFields(f).WithError(parseIntErr).Warnf("problem converting repository %s external ID string value: %s to integer", gitLabProjectDBRecord.RepositoryFullPath, gitLabProjectDBRecord.RepositoryExternalID) + } else { + deleteErr := v2RepositoriesService.GitLabDeleteRepositoryByExternalID(ctx, repositoryExternalID) + if deleteErr != nil { + log.WithFields(f).WithError(deleteErr).Warnf("problem deleting repository %s external ID string value: %s to integer", gitLabProjectDBRecord.RepositoryFullPath, gitLabProjectDBRecord.RepositoryExternalID) + } else { + log.WithFields(f).Debugf("deleted GitLab project %s for group/organization: %s from the database", gitLabProjectDBRecord.RepositoryName, gitLabGroup.OrganizationFullPath) + } + } + } + } + + log.WithFields(f).Debugf("done - processed GitLab group/organization: %s with group ID: %d associated with project SFID: %s", gitLabGroup.OrganizationURL, gitLabGroup.OrganizationExternalID, gitLabGroup.ProjectSfid) + } + + log.WithFields(f).Debugf("done - checked %d GitLab projects for add/delete events", len(gitLabGroups.List)) + return nil +} + +// getNewProjects is a helper function to determine if we have any new GitLab projects that are not in our database +func getNewProjects(gitLabProjects []*goGitLab.Project, gitLabDBProjects []*v1Repositories.RepositoryDBModel) []*goGitLab.Project { + var response []*goGitLab.Project + f := logrus.Fields{ + "functionName": "getNewProjects", + } + if len(gitLabDBProjects) == 0 { + // No projects in the database - return all the projects from GitLab + log.WithFields(f).Debugf("no projects in the database - returning all projects from GitLab: %+v", gitLabProjects) + return gitLabProjects + } + + // For each GitLab Project/Repo + for _, gitLabProject := range gitLabProjects { + found := false + + // For each GitLab Project/Repo in the database + for _, gitLabDBProject := range gitLabDBProjects { + // Compare the full name/path + if strings.ToLower(gitLabProject.PathWithNamespace) == strings.ToLower(gitLabDBProject.RepositoryFullPath) { + found = true + break + } + } + + // Didn't find the GitLab Project Repo from GitLab defined in our database - must have been added! + if !found { + // Add to our list + response = append(response, gitLabProject) + } + } + + return response +} + +// getDeletedProjects is a helper function to determine if we have any new GitLab projects that were removed from GitLab but are still in our database +func getDeletedProjects(gitLabProjects []*goGitLab.Project, gitLabDBProjects []*v1Repositories.RepositoryDBModel) []*v1Repositories.RepositoryDBModel { + response := make([]*v1Repositories.RepositoryDBModel, 0) + f := logrus.Fields{ + "functionName": "getDeletedProjects", + } + + if len(gitLabProjects) == 0 { + // No projects in GitLab - return all the projects from the database + log.WithFields(f).Debugf("no projects in GitLab - returning all projects from the database: %+v", gitLabDBProjects) + return gitLabDBProjects + } + + log.WithFields(f).Debugf("len(gitLabProjects): %d and len(gitLabDbProjects): %d", len(gitLabProjects), len(gitLabDBProjects)) + + // For each GitLab Project/Repo in the database + for _, gitLabDBProject := range gitLabDBProjects { + found := false + + // For each GitLab Project/Repo + for _, gitLabProject := range gitLabProjects { + // Compare the full name/path + log.WithFields(f).Debugf("comparing GitLab project: %s with GitLab DB project: %s", gitLabProject.PathWithNamespace, gitLabDBProject.RepositoryFullPath) + if strings.ToLower(gitLabProject.PathWithNamespace) == strings.ToLower(gitLabDBProject.RepositoryFullPath) { + found = true + break + } + + } + + // Didn't find the GitLab Project Repo from the database defined in GitLab - must have been removed! + if !found { + // Add to our list + log.WithFields(f).Debugf("adding GitLab project: %s to the list of projects to be deleted", gitLabDBProject.RepositoryFullPath) + response = append(response, gitLabDBProject) + } else { + log.WithFields(f).Debugf("GitLab project: %s was not found in the list of projects to be deleted", gitLabDBProject.RepositoryFullPath) + } + } + + return response +} diff --git a/cla-backend-go/cmd/gitlab_repository_check/handler/server_aws_lambda.go b/cla-backend-go/cmd/gitlab_repository_check/handler/server_aws_lambda.go new file mode 100644 index 000000000..01eb964bb --- /dev/null +++ b/cla-backend-go/cmd/gitlab_repository_check/handler/server_aws_lambda.go @@ -0,0 +1,23 @@ +//go:build aws_lambda +// +build aws_lambda + +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package handler + +import ( + "github.com/aws/aws-lambda-go/lambda" + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/sirupsen/logrus" +) + +// RunHandler starts the lambda main handler routine +func RunHandler() { + f := logrus.Fields{ + "functionName": "cmd.gitlab_repository_check.handler.RunHandler", + } + log.WithFields(f).Info("lambda server starting...") + lambda.Start(Handler) + log.WithFields(f).Infof("Lambda shutting down...") +} diff --git a/cla-backend-go/cmd/gitlab_repository_check/handler/server_standalone.go b/cla-backend-go/cmd/gitlab_repository_check/handler/server_standalone.go new file mode 100644 index 000000000..4cf227191 --- /dev/null +++ b/cla-backend-go/cmd/gitlab_repository_check/handler/server_standalone.go @@ -0,0 +1,26 @@ +//go:build !aws_lambda +// +build !aws_lambda + +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package handler + +import ( + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/sirupsen/logrus" +) + +// RunHandler starts the lambda in local testing model by invoking the handler directly +func RunHandler() { + f := logrus.Fields{ + "functionName": "cmd.gitlab_repository_check.handler.RunHandler", + } + log.WithFields(f).Debug("creating a new handler") + err := Handler(utils.NewContext()) + if err != nil { + log.WithFields(f).WithError(err).Warn("error returned from handler") + } + log.Infof("handler completed") +} diff --git a/cla-backend-go/cmd/gitlab_repository_check/main.go b/cla-backend-go/cmd/gitlab_repository_check/main.go new file mode 100644 index 000000000..5b2723e97 --- /dev/null +++ b/cla-backend-go/cmd/gitlab_repository_check/main.go @@ -0,0 +1,11 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package main + +import "github.com/communitybridge/easycla/cla-backend-go/cmd/gitlab_repository_check/handler" + +func main() { + handler.Init() + handler.RunHandler() +} diff --git a/cla-backend-go/cmd/ldap_gerrit_check/main.go b/cla-backend-go/cmd/ldap_gerrit_check/main.go new file mode 100644 index 000000000..76fdca97c --- /dev/null +++ b/cla-backend-go/cmd/ldap_gerrit_check/main.go @@ -0,0 +1,173 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package main + +import ( + // "context" + "encoding/csv" + "flag" + "fmt" + "os" + "path/filepath" + "sync" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/communitybridge/easycla/cla-backend-go/events" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + eventOps "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/events" + log "github.com/communitybridge/easycla/cla-backend-go/logging" + // "github.com/communitybridge/easycla/cla-backend-go/users" +) + +var awsSession = session.Must(session.NewSession(&aws.Config{})) +var stage string + +func main() { + stage = os.Getenv("STAGE") + if stage == "" { + log.Fatal("stage not set") + } + log.Infof("STAGE set to %s\n", stage) + + var wg sync.WaitGroup + var mu sync.Mutex + + // Initialize the events repository + eventsRepo := events.NewRepository(awsSession, stage) + eventService := events.NewService(eventsRepo, nil) + + // Initialize the users repository + // usersRepo := users.NewRepository(awsSession, stage) + + inputFilename := flag.String("input-file", "", "Input with a given list of lf usernames") + claGroup := flag.String("cla-group-id", "", "The ID of the CLA group") + claGroupName := flag.String("cla-group-name", "", "The name of the CLA group") + flag.Parse() + + if *inputFilename == "" || *claGroup == "" { + log.Fatalf("Both input-file and cla-group are required") + } + + log.Debugf("Input file: %s", *inputFilename) + + file, err := os.Open(*inputFilename) + if err != nil { + log.Fatalf("Unable to read input file: %s", *inputFilename) + } + + defer func() { + if err = file.Close(); err != nil { + log.Fatalf("Error closing file: %v", err) + } + }() + + reader := csv.NewReader(file) + + records, err := reader.ReadAll() + if err != nil { + log.Fatalf("Unable to read file") + } + + log.Debugf("CLA Group Name: %s", *claGroup) + + type Report struct { + Username string + Events []*models.Event + } + + projectReport := make([]Report, 0) + + for i, record := range records { + if i == 0 { + continue + } + lfUsername := record[0] + log.Debugf("Processing record: %s", lfUsername) + report := Report{ + Username: lfUsername, + } + + // Increment the wait group + wg.Add(1) + + go func(lfusername string) { + defer wg.Done() + log.Debugf("Processing record: %s", lfusername) + searchParams := eventOps.SearchEventsParams{ + SearchTerm: &lfusername, + ProjectID: claGroup, + } + events, eventErr := eventService.SearchEvents(&searchParams) + if eventErr != nil { + log.Debugf("Error getting events: %v", eventErr) + report.Events = nil + } + + if len(events.Events) == 0 { + log.Warnf("No events found for user: %s", lfusername) + report.Events = nil + } else { + log.Debugf("Events found for user: %s", lfusername) + report.Events = events.Events + } + + mu.Lock() + projectReport = append(projectReport, report) + defer mu.Unlock() + + }(lfUsername) + } + + // Wait for all the go routines to finish + wg.Wait() + + // Create a csv file with the results + outputFilename := fmt.Sprintf("ldap-%s-%s.csv", *claGroupName, time.Now().Format("2006-01-02-15-04-05")) + outputFile, err := os.Create(filepath.Clean(outputFilename)) + + if err != nil { + log.Fatalf("Unable to create output file: %s", outputFilename) + } + + defer func() { + if err = outputFile.Close(); err != nil { + log.Fatalf("Error closing file: %v", err) + } + }() + + writer := csv.NewWriter(outputFile) + + err = writer.Write([]string{"Username", "Event ID", "Event Data", "Event Type", "Event Date"}) + if err != nil { + log.Fatalf("Error writing csv: %v", err) + } + + for _, report := range projectReport { + if report.Events == nil { + err = writer.Write([]string{report.Username, "No events found", "", "", ""}) + if err != nil { + log.Fatalf("Error writing csv: %v", err) + } + continue + } + for _, event := range report.Events { + err = writer.Write([]string{report.Username, event.EventID, event.EventData, event.EventType, event.EventTime}) + if err != nil { + log.Fatalf("Error writing csv: %v", err) + } + + } + } + + writer.Flush() + + if err := writer.Error(); err != nil { + log.Fatalf("Error writing csv: %v", err) + } + + log.Infof("Output written to: %s", outputFilename) + +} diff --git a/cla-backend-go/cmd/migrate_approval_list/main.go b/cla-backend-go/cmd/migrate_approval_list/main.go new file mode 100644 index 000000000..63f6c387f --- /dev/null +++ b/cla-backend-go/cmd/migrate_approval_list/main.go @@ -0,0 +1,300 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + + // "strings" + "sync" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/gofrs/uuid" + "github.com/sirupsen/logrus" + + "github.com/communitybridge/easycla/cla-backend-go/company" + "github.com/communitybridge/easycla/cla-backend-go/events" + v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + "github.com/communitybridge/easycla/cla-backend-go/gerrits" + "github.com/communitybridge/easycla/cla-backend-go/github_organizations" + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/project/repository" + "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" + "github.com/communitybridge/easycla/cla-backend-go/repositories" + "github.com/communitybridge/easycla/cla-backend-go/signatures" + "github.com/communitybridge/easycla/cla-backend-go/users" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/communitybridge/easycla/cla-backend-go/v2/approvals" +) + +var stage string +var approvalRepo approvals.IRepository +var signatureRepo signatures.SignatureRepository +var eventsRepo events.Repository +var usersRepo users.UserRepository +var eventsService events.Service +var awsSession = session.Must(session.NewSession(&aws.Config{})) +var approvalsTableName string +var companyRepo company.IRepository +var v1ProjectClaGroupRepo projects_cla_groups.Repository +var ghRepo repositories.Repository +var gerritsRepo gerrits.Repository +var ghOrgRepo github_organizations.Repository +var gerritService gerrits.Service +var eventsByType []*v1Models.Event +var toUpdateApprovalItems []approvals.ApprovalItem + +type combinedRepo struct { + users.UserRepository + company.IRepository + repository.ProjectRepository + projects_cla_groups.Repository +} + +func init() { + stage = os.Getenv("STAGE") + if stage == "" { + log.Fatal("stage not set") + } + + log.Infof("STAGE set to %s\n", stage) + approvalsTableName = fmt.Sprintf("cla-%s-approvals", stage) + approvalRepo = approvals.NewRepository(stage, awsSession, approvalsTableName) + eventsRepo = events.NewRepository(awsSession, stage) + usersRepo = users.NewRepository(awsSession, stage) + companyRepo = company.NewRepository(awsSession, stage) + ghRepo = *repositories.NewRepository(awsSession, stage) + gerritsRepo = gerrits.NewRepository(awsSession, stage) + v1CLAGroupRepo := repository.NewRepository(awsSession, stage, &ghRepo, gerritsRepo, v1ProjectClaGroupRepo) + v1ProjectClaGroupRepo = projects_cla_groups.NewRepository(awsSession, stage) + eventsService = events.NewService(eventsRepo, combinedRepo{ + usersRepo, + companyRepo, + v1CLAGroupRepo, + v1ProjectClaGroupRepo, + }) + ghOrgRepo = github_organizations.NewRepository(awsSession, stage) + gerritService = gerrits.NewService(gerritsRepo) + signatureRepo = signatures.NewRepository(awsSession, stage, companyRepo, usersRepo, eventsService, &ghRepo, ghOrgRepo, gerritService, approvalRepo) + + log.Info("initialized repositories\n") +} + +func main() { + f := logrus.Fields{ + "functionName": "main", + } + log.WithFields(f).Info("Starting migration") + log.Info("Fetching ccla signatures") + signed := true + approved := true + // signatureID := flag.String("signature-id", "ALL", "signature ID to migrate") + delete := flag.Bool("delete", false, "delete approval items") + signatureID := flag.String("signature-id", "ALL", "signature ID to migrate") + flag.Parse() + + if *delete { + log.Info("Deleting approval items") + err := approvalRepo.BatchDeleteApprovalList() + if err != nil { + log.WithFields(f).WithError(err).Error("error deleting approval items") + return + } + log.Info("Deleted all approval items") + return + + } else if *signatureID != "ALL" { + log.Infof("Migrating approval items for signature : %s", *signatureID) + signature, err := signatureRepo.GetItemSignature(context.Background(), *signatureID) + if err != nil { + log.WithFields(f).WithError(err).Errorf("error fetching signature : %s", *signatureID) + return + } + log.WithFields(f).Debugf("Processing signature : %+v", signature) + err = updateApprovalsTable(signature) + if err != nil { + log.WithFields(f).WithError(err).Errorf("error updating approvals table for signature : %s", *signatureID) + return + } + log.Infof("batch update %d approvals ", len(toUpdateApprovalItems)) + err = approvalRepo.BatchAddApprovalList(toUpdateApprovalItems) + if err != nil { + log.WithFields(f).WithError(err).Error("error adding approval items") + return + } + return + } + + log.Info("Fetching all ccla signatures...") + cclaSignatures, err := signatureRepo.GetCCLASignatures(context.Background(), &signed, &approved) + if err != nil { + log.Fatalf("Error fetching ccla signatures : %v", err) + } + log.Infof("Fetched %d ccla signatures", len(cclaSignatures)) + + log.WithFields(f).Debugf("Fetching events by type : %s", events.ClaApprovalListUpdated) + eventsByType, err = eventsRepo.GetEventsByType(events.ClaApprovalListUpdated, 100) + + if err != nil { + log.WithFields(f).WithError(err).Errorf("error fetching events by type : %s", events.ClaApprovalListUpdated) + return + } + + var wg sync.WaitGroup + + for _, cclaSignature := range cclaSignatures { + wg.Add(1) + go func(signature *signatures.ItemSignature) { + defer wg.Done() + log.WithFields(f).Debugf("Processing company : %s, project : %s", signature.SignatureReferenceName, signature.SignatureProjectID) + updateErr := updateApprovalsTable(signature) + if updateErr != nil { + log.WithFields(f).Warnf("Error updating approvals table for signature : %s, error: %v", signature.SignatureID, updateErr) + } + }(cclaSignature) + } + wg.Wait() + log.WithFields(f).Infof("batch update %d approvals ", len(toUpdateApprovalItems)) + err = approvalRepo.BatchAddApprovalList(toUpdateApprovalItems) + if err != nil { + log.WithFields(f).WithError(err).Error("error adding approval items") + return + } + +} + +func getSearchTermEvents(events []*v1Models.Event, searchTerm, companyID, claGroupID string) []*v1Models.Event { + f := logrus.Fields{ + "functionName": "getSearchTermEvents", + "searchTerm": searchTerm, + "companyID": companyID, + } + log.WithFields(f).Debug("searching for events ...") + var result []*v1Models.Event + for _, event := range events { + if strings.Contains(strings.ToLower(event.EventData), strings.ToLower(searchTerm)) && event.EventCompanyID == companyID && event.EventCLAGroupID == claGroupID { + log.WithFields(f).Debugf("found event with search term : %s", searchTerm) + result = append(result, event) + } + } + return result +} + +func updateApprovalsTable(signature *signatures.ItemSignature) error { + f := logrus.Fields{ + "functionName": "updateApprovalsTable", + "signatureID": signature.SignatureID, + "companyName": signature.SignatureReferenceName, + } + log.WithFields(f).Debugf("updating approvals table for signature : %s", signature.SignatureID) + var wg sync.WaitGroup + var errMutex sync.Mutex + var err error + + update := func(approvalList []string, listType string) { + defer wg.Done() + for _, item := range approvalList { + searchIdentifier := "" + switch listType { + case utils.DomainApprovalCriteria: + searchIdentifier = "email address domain" + case utils.EmailApprovalCriteria: + searchIdentifier = "email address" + case utils.GithubUsernameApprovalCriteria: + searchIdentifier = "GitHub username" + case utils.GitlabUsernameApprovalCriteria: + searchIdentifier = "GitLab username" + case utils.GithubOrgApprovalCriteria: + searchIdentifier = "GitHub organization" + case utils.GitlabOrgApprovalCriteria: + searchIdentifier = "GitLab group" + default: + searchIdentifier = "" + } + searchTerm := fmt.Sprintf("%s %s was added to the approval list", searchIdentifier, item) + events := getSearchTermEvents(eventsByType, searchTerm, signature.SignatureReferenceID, signature.SignatureProjectID) + dateAdded := signature.DateModified + + if len(events) > 0 { + latestEvent := getLatestEvent(events) + dateAdded = latestEvent.EventTime + } + + approvalID, approvalErr := uuid.NewV4() + if err != nil { + errMutex.Lock() + err = approvalErr + log.WithFields(f).Warnf("Error creating new UUIDv4, error: %v", err) + errMutex.Unlock() + return + } + currentTime := time.Now().UTC().String() + note := fmt.Sprintf("Approval item added by migration script on %s", currentTime) + approvalItem := approvals.ApprovalItem{ + ApprovalID: approvalID.String(), + SignatureID: signature.SignatureID, + DateCreated: currentTime, + DateModified: currentTime, + ApprovalName: item, + ApprovalCriteria: listType, + CompanyID: signature.SignatureReferenceID, + ProjectID: signature.SignatureProjectID, + ApprovalCompanyName: signature.SignatureReferenceName, + DateAdded: dateAdded, + Note: note, + Active: true, + } + + toUpdateApprovalItems = append(toUpdateApprovalItems, approvalItem) + } + } + + wg.Add(1) + go update(signature.EmailDomainApprovalList, utils.DomainApprovalCriteria) + + wg.Add(1) + go update(signature.EmailApprovalList, utils.EmailApprovalCriteria) + + wg.Add(1) + go update(signature.GitHubOrgApprovalList, utils.GithubOrgApprovalCriteria) + + wg.Add(1) + go update(signature.GitHubUsernameApprovalList, utils.GithubUsernameApprovalCriteria) + + wg.Add(1) + go update(signature.GitlabOrgApprovalList, utils.GitlabOrgApprovalCriteria) + + wg.Add(1) + go update(signature.GitlabUsernameApprovalList, utils.GitlabUsernameApprovalCriteria) + + wg.Wait() + + return err +} + +func getLatestEvent(events []*v1Models.Event) *v1Models.Event { + var latest *v1Models.Event + var latestTime time.Time + + for _, item := range events { + t, err := utils.ParseDateTime(item.EventTime) + if err != nil { + log.Debugf("Error parsing time: %+v ", err) + continue + } + + if latest == nil || t.After(latestTime) { + latest = item + latestTime = t + } + } + + return latest +} diff --git a/cla-backend-go/cmd/org_report/main.go b/cla-backend-go/cmd/org_report/main.go new file mode 100644 index 000000000..e76f0038a --- /dev/null +++ b/cla-backend-go/cmd/org_report/main.go @@ -0,0 +1,362 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "encoding/csv" + "encoding/xml" + "fmt" + "os" + "strings" + "sync" + "time" + + // "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/communitybridge/easycla/cla-backend-go/company" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + sigParams "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/signatures" + "github.com/communitybridge/easycla/cla-backend-go/gerrits" + "github.com/communitybridge/easycla/cla-backend-go/users" + log "github.com/communitybridge/easycla/cla-backend-go/logging" + cla_group "github.com/communitybridge/easycla/cla-backend-go/project/repository" + "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" + "github.com/communitybridge/easycla/cla-backend-go/repositories" + "github.com/communitybridge/easycla/cla-backend-go/signatures" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/sirupsen/logrus" +) + +const ( + batchSize = 100 + signatureBatchSize = 100 + maxConcurrentGoroutines = 20 // Control concurrency +) + +var ( + awsSession = session.Must(session.NewSession()) + stage string + companyRepo company.IRepository + projectRepo cla_group.ProjectRepository + signatureRepo signatures.SignatureRepository +) + +func init() { + stage = os.Getenv("STAGE") + if stage == "" { + log.Fatal("stage not set") + } + log.Infof("STAGE set to %s\n", stage) + companyRepo = company.NewRepository(awsSession, stage) + ghRepo := repositories.NewRepository(awsSession, stage) + gerritRepo := gerrits.NewRepository(awsSession, stage) + pcgRepo := projects_cla_groups.NewRepository(awsSession, stage) + projectRepo = cla_group.NewRepository(awsSession, stage, ghRepo, gerritRepo, pcgRepo) + userRepo := users.NewRepository(awsSession, stage) + signatureRepo = signatures.NewRepository(awsSession, stage, companyRepo, userRepo, nil, nil, nil, nil, nil) +} + +func main() { + f := logrus.Fields{ + "functionName": "main", + "stage": stage, + } + log.WithFields(f).Info("loading company data...") + + // Load the company data + companyData, err := companyRepo.GetCompanies(context.Background()) + if err != nil { + log.Warnf("Unable to load company data, error: %+v", err) + return + } + + if len(companyData.Companies) == 0 { + log.Warn("No companies found") + return + } + + var companyDataList []CompanyData + + log.WithFields(f).Infof("processing %d companies...", len(companyData.Companies)) + + processCompaniesBatch(companyData.Companies, &companyDataList) + err = exportToCSV(companyDataList) + if err != nil { + log.Warnf("Unable to export company data to csv, error: %+v", err) + return + } + + // // Process the companies in batches + // for i := 0; i < len(companyData.Companies); i += batchSize { + // end := i + batchSize + // if end > len(companyData.Companies) { + // end = len(companyData.Companies) + // } + // processCompaniesBatch(companyData.Companies[i:end], &companyDataList) + // log.WithFields(f).Info("exporting company data to csv...") + // err = exportToCSV(companyDataList) + // if err != nil { + // log.Warnf("Unable to export company data to csv, error: %+v", err) + // return + // } + // } + +} + +func processCompaniesBatch(companiesBatch []models.Company, companyDataList *[]CompanyData) { + f := logrus.Fields{ + "functionName": "processCompaniesBatch", + } + + var wg sync.WaitGroup + var mu sync.Mutex + + // Semaphore channel to control the number of concurrent goroutines + sem := make(chan struct{}, maxConcurrentGoroutines) + + for _, company := range companiesBatch { + wg.Add(1) + go func(companyID string) { + defer wg.Done() + sem <- struct{}{} // Acquire semaphore + defer func() { <-sem }() // Release semaphore + + log.WithFields(f).Infof("processing company: %s", companyID) + data, err := processCompany(companyID) + if err != nil { + log.Warnf("Unable to process company data, error: %+v", err) + return + } + // log.WithFields(f).Infof("processed company data: %+v", data) + mu.Lock() + if data != nil && len(data.ClaManagers) > 0 { + *companyDataList = append(*companyDataList, *data) + } else { + log.Warn("No ccla signatures found for company") + } + mu.Unlock() + }(company.CompanyID) + } + + wg.Wait() +} + +func processCompany(companyID string) (*CompanyData, error) { + f := logrus.Fields{ + "functionName": "processCompany", + "companyID": companyID, + } + + var companyData CompanyData + + log.WithFields(f).Info("loading company data...") + companyModel, err := companyRepo.GetCompany(context.Background(), companyID) + if err != nil { + log.Warnf("Unable to load company data, error: %+v", err) + return nil, err + } + + params := sigParams.GetCompanySignaturesParams{ + CompanyID: companyModel.CompanyID, + } + + companySignatures, err := signatureRepo.GetCompanySignatures(context.Background(), params, 10, false) + if err != nil { + log.Warnf("Unable to load CCLA signatures, error: %+v", err) + return nil, err + } + + if len(companySignatures.Signatures) == 0 { + log.Warn("No CCLA signatures found") + return nil, nil + } + + companyData.CompanyID = companyModel.CompanyID + companyData.CompanyName = companyModel.CompanyName + companyData.CompanySFID = companyModel.CompanyExternalID + + + log.WithFields(f).Info("processing CCLA signatures...") + populateCompanyDataFromSignatures(&companyData, companySignatures.Signatures) + + return &companyData, nil +} + +func populateCompanyDataFromSignatures(companyData *CompanyData, cclaSignatures []*models.Signature) { + var mu sync.Mutex + var wg sync.WaitGroup + + claGroupNames := []string{} + addresses := []string{} + cclaManagers := []string{} + + for _, sig := range cclaSignatures { + if companyData.DateFirstSigned == "" || sig.SignedOn < companyData.DateFirstSigned { + companyData.DateFirstSigned = sig.SignedOn + } + if companyData.DateLastSigned == "" || sig.SignedOn > companyData.DateLastSigned { + companyData.DateLastSigned = sig.SignedOn + } + + for _, manager := range sig.SignatureACL { + if !utils.StringInSlice(manager.LfUsername, cclaManagers) { + cclaManagers = append(cclaManagers, manager.LfUsername) + } + } + + // CLA Group Names and Corporation Addresses + wg.Add(2) + + // CLA Group Name + go func(claGroupID string) { + defer wg.Done() + loadDetails := false + claGroupModel, err := projectRepo.GetCLAGroupByID(context.Background(), claGroupID, loadDetails) + if err != nil { + log.Warnf("unable to load CLA group, error: %+v", err) + return + } + mu.Lock() + claGroupNames = append(claGroupNames, claGroupModel.ProjectName) + mu.Unlock() + }(sig.ProjectID) + + // Corporation Address + go func(xmlData string) { + defer wg.Done() + signature, err := signatureRepo.GetItemSignature(context.Background(), sig.SignatureID) + if err != nil { + log.Warnf("unable to load signature, error: %+v", err) + return + } + + if signature.UserDocusignRawXML == "" { + log.Warn("user docusign raw xml is empty") + return + } + + log.Info("parsing xml data...") + companyAddress, err := parseXML(signature.UserDocusignRawXML) + if err != nil { + log.Warnf("unable to parse xml data, error: %+v", err) + return + } + mu.Lock() + addresses = append(addresses, companyAddress) + mu.Unlock() + }(sig.SignatureID) + } + + wg.Wait() + companyData.ClaGroupNames = claGroupNames + companyData.CoporationAddress = addresses + companyData.ClaManagers = cclaManagers +} + +func parseXML(xmlData string) (string, error) { + f := logrus.Fields{ + "functionName": "parseXML", + } + var companyAddress string + // Parse the XML data + var envelopeInformation DocusignEnvelopeInformation + log.WithFields(f).Info("unmarshalling xml data...") + err := xml.Unmarshal([]byte(xmlData), &envelopeInformation) + if err != nil { + log.Warnf("unable to unmarshal xml data, error: %+v", err) + return companyAddress, err + } + + // Extract the corporation address + var addressParts []string + for _, recipientStatus := range envelopeInformation.EnvelopeStatus.RecipientStatuses.RecipientStatus { + for _, field := range recipientStatus.FormData.XFDF.Fields.Field { + switch field.Name { + case "corporation_address1", "corporation_address2", "corporation_address3": + if field.Value != "" && strings.ToLower(field.Value) != "none" { + log.WithFields(f).Infof("adding address part: %s", field.Value) + addressParts = append(addressParts, field.Value) + } + } + } + } + + companyAddress = strings.Join(addressParts, ", ") + log.WithFields(f).Infof("company address: %s", companyAddress) + return companyAddress, nil +} + +func exportToCSV(companyData []CompanyData) error { + f := logrus.Fields{ + "functionName": "exportToCSV", + } + log.WithFields(f).Info("exporting company data to csv...") + // Export the data to CSV + // create the file with a timestamp + file, err := os.Create(fmt.Sprintf("company_data_%s.csv", time.Now().Format("2006-01-02T15:04:05"))) + if err != nil { + log.Warnf("unable to create file, error: %+v", err) + + return err + } + + // defer file.Close() + defer func() { + if err = file.Close(); err != nil { + log.Warnf("unable to close file, error: %+v", err) + } + }() + + w := csv.NewWriter(file) + defer w.Flush() + + // Write the header + headers := []string{ + "Company ID", + "Company Name", + "Company SFID", + "CCLA Signatures", + "Date First Signed", + "Date Last Signed", + "Corporation Address", + "CLA Managers", + "CLA Group Names", + } + + err = w.Write(headers) + if err != nil { + log.Warnf("unable to write headers, error: %+v", err) + return err + } + + // Write the data + for _, data := range companyData { + address := " " + if len(data.CoporationAddress) > 0 { + address = data.CoporationAddress[0] + } + record := []string{ + data.CompanyID, + data.CompanyName, + data.CompanySFID, + fmt.Sprintf("%d", len(data.ClaManagers)), + data.DateFirstSigned, + data.DateLastSigned, + address, + strings.Join(data.ClaManagers, ", "), + strings.Join(data.ClaGroupNames, ", "), + } + + log.WithFields(f).Infof("writing record: %+v", record) + err = w.Write(record) + if err != nil { + log.Warnf("unable to write record, error: %+v", err) + return err + } + } + + log.WithFields(f).Info("exported company data to csv") + return nil +} diff --git a/cla-backend-go/cmd/org_report/org_models.go b/cla-backend-go/cmd/org_report/org_models.go new file mode 100644 index 000000000..72c49ee72 --- /dev/null +++ b/cla-backend-go/cmd/org_report/org_models.go @@ -0,0 +1,53 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package main + +import "encoding/xml" + +type DocusignEnvelopeInformation struct { + XMLName xml.Name `xml:"DocuSignEnvelopeInformation"` + EnvelopeStatus EnvelopeStatus `xml:"EnvelopeStatus"` +} + +type EnvelopeStatus struct { + RecipientStatuses RecipientStatuses `xml:"RecipientStatuses"` +} + +type RecipientStatuses struct { + RecipientStatus []RecipientStatus `xml:"RecipientStatus"` +} + +type RecipientStatus struct { + FormData FormData `xml:"FormData"` +} + +type FormData struct { + XFDF XFDF `xml:"xfdf"` +} + +type XFDF struct { + Fields Fields `xml:"fields"` +} + +type Fields struct { + Field []Field `xml:"field"` +} + +type Field struct { + Name string `xml:"name,attr"` + Value string `xml:"value"` +} + +// Struct to hold the final data +type CompanyData struct { + CompanyID string + CompanyName string + CompanySFID string + CCLASignatures int + DateFirstSigned string + DateLastSigned string + CoporationAddress []string + ClaManagers []string + ClaGroupNames []string +} diff --git a/cla-backend-go/cmd/repository_project_update/main.go b/cla-backend-go/cmd/repository_project_update/main.go new file mode 100644 index 000000000..24670d6a7 --- /dev/null +++ b/cla-backend-go/cmd/repository_project_update/main.go @@ -0,0 +1,136 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "fmt" + "os" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" + "github.com/aws/aws-sdk-go/service/dynamodb/expression" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" +) + +var awsSession = session.Must(session.NewSession(&aws.Config{})) +var gitHubRepo RepositoryInterface +var stage string + +type RepositoryInterface interface { + UpdateRepository(ctx context.Context, repositoryID string) error + GetDisabledRepositories(ctx context.Context) ([]*Repository, error) +} + +type repo struct { + tableName string + dynamoDBClient *dynamodb.DynamoDB + stage string +} + +type Repository struct { + RepositoryID string `json:"repository_id"` + Enabled bool `json:"enabled"` + ProjectSFID string `json:"project_sfid"` + ParentProjectSFID string `json:"repository_sfdc_id"` +} + +func (repo *repo) UpdateRepository(ctx context.Context, repositoryID string) error { + updateExpression := expression.Remove(expression.Name("project_sfid")).Remove(expression.Name("repository_sfdc_id")) + expr, err := expression.NewBuilder().WithUpdate(updateExpression).Build() + if err != nil { + return err + } + + _, err = repo.dynamoDBClient.UpdateItemWithContext(ctx, &dynamodb.UpdateItemInput{ + TableName: aws.String(repo.tableName), + Key: map[string]*dynamodb.AttributeValue{ + "repository_id": { + S: aws.String(repositoryID), + }, + }, + UpdateExpression: expr.Update(), + ExpressionAttributeNames: expr.Names(), + }) + if err != nil { + return err + } + + log.Debugf("Updated repository: %s", repositoryID) + + return nil +} + +func (repo *repo) GetDisabledRepositories(ctx context.Context) ([]*Repository, error) { + builder := expression.NewBuilder() + filter := expression.Name("enabled").Equal(expression.Value(false)).And(expression.Name("project_sfid").AttributeExists()).And(expression.Name("repository_sfdc_id").AttributeExists()) + builder = builder.WithFilter(filter) + expr, err := builder.Build() + if err != nil { + return nil, err + } + result, err := repo.dynamoDBClient.ScanWithContext(ctx, &dynamodb.ScanInput{ + TableName: aws.String(repo.tableName), + FilterExpression: expr.Filter(), + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + }) + + if err != nil { + return nil, err + } + + if len(result.Items) == 0 { + log.Warn("No disabled repositories found") + return nil, nil + } else { + log.Debugf("Found %d disabled repositories", len(result.Items)) + } + + var out []*Repository + err = dynamodbattribute.UnmarshalListOfMaps(result.Items, &out) + if err != nil { + return nil, err + } + + return out, nil +} + +func NewRepository(awsSession *session.Session, stage string) RepositoryInterface { + return &repo{ + tableName: fmt.Sprintf("cla-%s-repositories", stage), + dynamoDBClient: dynamodb.New(awsSession), + stage: stage, + } +} + +func init() { + stage = os.Getenv("STAGE") + if stage == "" { + log.Fatal("stage not set") + } + log.Infof("STAGE set to %s\n", stage) + + gitHubRepo = NewRepository(awsSession, stage) +} + +func main() { + log.Debugf("Getting disabled repositories that have project details...") + context := context.Background() + disabledRepos, err := gitHubRepo.GetDisabledRepositories(context) + if err != nil { + log.Fatalf("Unable to get disabled repositories, error: %v", err) + } + log.Debugf("disabled repositories with existing project details: %v", disabledRepos) + for _, repo := range disabledRepos { + log.Debugf("Updating repository: %+v", *repo) + err := gitHubRepo.UpdateRepository(context, repo.RepositoryID) + if err != nil { + log.Fatalf("Unable to update repository: %s, error: %v", repo.RepositoryID, err) + } + } +} diff --git a/cla-backend-go/cmd/s3_upload/main.go b/cla-backend-go/cmd/s3_upload/main.go new file mode 100644 index 000000000..981eb4733 --- /dev/null +++ b/cla-backend-go/cmd/s3_upload/main.go @@ -0,0 +1,374 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "encoding/csv" + "encoding/xml" + "strings" + "sync" + + "flag" + + "os" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + + "github.com/communitybridge/easycla/cla-backend-go/company" + "github.com/communitybridge/easycla/cla-backend-go/config" + "github.com/communitybridge/easycla/cla-backend-go/github_organizations" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/signatures" + "github.com/communitybridge/easycla/cla-backend-go/users" + "github.com/communitybridge/easycla/cla-backend-go/v2/sign" + + "github.com/communitybridge/easycla/cla-backend-go/utils" + + "github.com/sirupsen/logrus" +) + +var stage string +var signatureRepo signatures.SignatureRepository +var awsSession = session.Must(session.NewSession(&aws.Config{})) +var companyRepo company.IRepository +var usersRepo users.UserRepository + +var signService sign.Service +var githubOrgService github_organizations.Service +var report []ReportData +var failed int = 0 +var success int = 0 + +func init() { + stage = os.Getenv("STAGE") + if stage == "" { + log.Fatal("STAGE environment variable not set") + } + + companyRepo = company.NewRepository(awsSession, stage) + usersRepo = users.NewRepository(awsSession, stage) + signatureRepo = signatures.NewRepository(awsSession, stage, companyRepo, usersRepo, nil, nil, nil, nil, nil) + githubOrgService = github_organizations.Service{} + configFile, err := config.LoadConfig("", awsSession, stage) + if err != nil { + log.Fatal(err) + } + signService = sign.NewService("", "", companyRepo, nil, nil, nil, nil, configFile.DocuSignPrivateKey, nil, nil, nil, nil, githubOrgService, nil, "", "", nil, nil, nil, nil, nil) + // projectRepo = repository.NewRepository(awsSession, stage, nil, nil, nil) + utils.SetS3Storage(awsSession, configFile.SignatureFilesBucket) +} + +const ( + // Approved Flag + Approved = true + // Signed Flag + Signed = true + + Failed = "failed" + Success = "success" + DocumentUploaded = "Document uploaded successfully" +) + +type ReportData struct { + SignatureID string + ProjectID string + ReferenceID string + ReferenceName string + EnvelopeID string + DocumentID string + Comment string + Status string +} + +type APIErrorResponse struct { + ErrorCode string `json:"errorCode"` + Message string `json:"message"` +} + +func main() { // nolint + ctx := context.Background() + f := logrus.Fields{ + "functionName": "main", + } + // var toUpdate []*signatures.ItemSignature + + dryRun := flag.Bool("dry-run", false, "dry run mode") + folder := flag.String("folder", "", "folder to upload the s3 documents") + meta := flag.String("meta", "", "meta data to upload the s3 documents") + + flag.Parse() + + // Fetch all the signatures from 2024-02-01T00:00:00.000Z + startDate := "2024-02-01T00:00:00.000Z" + + if dryRun != nil && *dryRun { + log.WithFields(f).Debug("dry-run mode enabled") + } + + if folder != nil && *folder != "" && meta != nil && *meta != "" { + log.WithFields(f).Debugf("folder: %s, meta: %s", *folder, *meta) + // var metaMap map[string]string + + // Read csv file + file, err := os.Open(*meta) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem opening meta file") + return + } + + reader := csv.NewReader(file) + records, err := reader.ReadAll() + + count := len(records) + log.WithFields(f).Debugf("processing %d records", count) + + passed := 0 + + if err != nil { + log.WithFields(f).WithError(err).Warn("problem reading meta file") + return + } + + var wg sync.WaitGroup + + // Limit the number of concurrent uploads + semaphore := make(chan struct{}, 5) + + for _, record := range records { + wg.Add(1) + semaphore <- struct{}{} + go func(record []string) { + defer wg.Done() + defer func() { <-semaphore }() + fileName := record[0] + envelopeID := record[1] + signatureID := record[2] + projectID := record[3] + referenceID := record[4] + log.WithFields(f).Debugf("uploading file: %s, envelopeID: %s, signatureID: %s, projectID: %s, referenceID: %s", fileName, envelopeID, signatureID, projectID, referenceID) + // Upload the file + file, err := os.Open(*folder + "/" + fileName) // nolint + if err != nil { + log.WithFields(f).WithError(err).Warn("problem opening file") + failed++ + return + } + + if dryRun != nil && *dryRun { + log.WithFields(f).Debugf("dry-run mode enabled, skipping file upload: %s", fileName) + return + } + + // Upload the document + log.WithFields(f).Debugf("uploading document for signature...: %s", signatureID) + + err = utils.UploadFileToS3(file, projectID, utils.ClaTypeICLA, referenceID, signatureID) + + if err != nil { + log.WithFields(f).WithError(err).Warn("problem uploading file") + failed++ + return + } + passed++ + + log.WithFields(f).Debugf("document uploaded for signature: %s", signatureID) + + }(record) + } + + wg.Wait() + + log.WithFields(f).Debug("completed processing files") + + log.WithFields(f).Debugf("total: %d, passed: %d, failed: %d", count, passed, failed) + return + } + + iclaSignatures, err := signatureRepo.GetICLAByDate(ctx, startDate) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem fetching ICLA signatures") + return + } + + log.WithFields(f).Debugf("processing %d ICLA signatures", len(iclaSignatures)) + toUpload := make([]signatures.ItemSignature, 0) + + var wg sync.WaitGroup + semaphore := make(chan struct{}, 20) + + for _, icla := range iclaSignatures { + wg.Add(1) + semaphore <- struct{}{} + go func(sig signatures.ItemSignature) { + defer wg.Done() + defer func() { <-semaphore }() + key := strings.Join([]string{"contract-group", sig.SignatureProjectID, utils.ClaTypeICLA, sig.SignatureReferenceID, sig.SignatureID}, "/") + ".pdf" + fileExists, fileErr := utils.DocumentExists(key) + if fileErr != nil { + log.WithFields(f).WithError(fileErr).Debugf("unable to check s3 key : %s", key) + return + } + if !fileExists { + log.WithFields(f).Debugf("document is not uploaded for key: %s", key) + toUpload = append(toUpload, sig) + } else { + log.WithFields(f).Debugf("key: %s exists", key) + } + }(icla) + + } + + log.WithFields(f).Debugf("checking icla signatures from :%s", startDate) + wg.Wait() + log.WithFields(f).Debugf("To upload %d icla signatures: ", len(toUpload)) + + // Upload the documents + for _, icla := range toUpload { + wg.Add(1) + semaphore <- struct{}{} + go func(sig signatures.ItemSignature) { + defer wg.Done() + + var documentID string + + reportData := ReportData{ + SignatureID: sig.SignatureID, + ProjectID: sig.SignatureProjectID, + ReferenceID: sig.SignatureReferenceID, + ReferenceName: sig.SignatureReferenceName, + EnvelopeID: sig.SignatureEnvelopeID, + } + + // get the document id + var info sign.DocuSignEnvelopeInformation + + if sig.UserDocusignRawXML == "" { + log.WithFields(f).Debugf("no raw xml found for signature: %s", sig.SignatureID) + reportData.Comment = "No raw xml found" + // Fetch documentID + documents, docErr := signService.GetEnvelopeDocuments(ctx, sig.SignatureEnvelopeID) + if docErr != nil { + log.WithFields(f).WithError(err).Debugf("unable to get documents for signature: %s", sig.SignatureID) + reportData.Comment = docErr.Error() + reportData.Status = Failed + report = append(report, reportData) + failed++ + return + } + if len(documents) == 0 { + log.WithFields(f).Debugf("no documents found for signature: %s", sig.SignatureID) + reportData.Comment = "No documents found" + reportData.Status = Failed + report = append(report, reportData) + failed++ + return + } + documentID = documents[0].DocumentId + log.WithFields(f).Debugf("document id fetched from docusign: %s", documentID) + } else { + err = xml.Unmarshal([]byte(sig.UserDocusignRawXML), &info) + if err != nil { + log.WithFields(f).WithError(err).Debugf("unable to unmarshal xml for signature: %s", sig.SignatureID) + reportData.Comment = err.Error() + reportData.Status = Failed + report = append(report, reportData) + failed++ + return + } + documentID = info.EnvelopeStatus.DocumentStatuses[0].ID + } + + log.WithFields(f).Debugf("document id: %s", documentID) + reportData.DocumentID = documentID + envelopeID := sig.SignatureEnvelopeID + log.WithFields(f).Debugf("envelope id: %s", envelopeID) + + if documentID == "" { + log.WithFields(f).Debugf("no document id found for signature: %s", sig.SignatureID) + reportData.Comment = "No document id found" + reportData.Status = Failed + report = append(report, reportData) + failed++ + return + } + + // get the document + document, docErr := signService.GetSignedDocument(ctx, envelopeID, documentID) + if docErr != nil { + log.WithFields(f).WithError(docErr).Debugf("unable to get document for signature: %s", sig.SignatureID) + reportData.Comment = docErr.Error() + reportData.Status = Failed + report = append(report, reportData) + failed++ + return + } + // upload the document + if dryRun != nil && *dryRun { + log.WithFields(f).Debugf("dry-run mode enabled, skipping document upload for signature: %s", sig.SignatureID) + log.WithFields(f).Debugf("document uploaded for signature: %s", sig.SignatureID) + reportData.Comment = DocumentUploaded + reportData.Status = Success + report = append(report, reportData) + return + } + + log.WithFields(f).Debugf("uploading document for signature...: %s", sig.SignatureID) + err = utils.UploadToS3(document, sig.SignatureProjectID, utils.ClaTypeICLA, sig.SignatureReferenceID, sig.SignatureID) + if err != nil { + log.WithFields(f).WithError(err).Debugf("unable to upload document for signature: %s", sig.SignatureID) + reportData.Comment = err.Error() + reportData.Status = Failed + failed++ + report = append(report, reportData) + return + } + + log.WithFields(f).Debugf("document uploaded for signature: %s", sig.SignatureID) + reportData.Comment = DocumentUploaded + reportData.Status = Success + success++ + + report = append(report, reportData) + + // release the semaphore + <-semaphore + + }(icla) + } + + wg.Wait() + + log.WithFields(f).Debug("completed processing ICLA signatures") + + file, err := os.Create("s3_upload_report.csv") + if err != nil { + log.WithFields(f).WithError(err).Warn("problem creating report file") + return + } + + writer := csv.NewWriter(file) + defer writer.Flush() + + err = writer.Write([]string{"SignatureID", "ProjectID", "ReferenceID", "ReferenceName", "EnvelopeID", "DocumentID", "Comment", "Status"}) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem writing header to report file") + return + } + + for _, data := range report { + // writer.Write([]string{data.SignatureID, data.ProjectID, data.ReferenceID, data.ReferenceName, data.EnvelopeID, data.DocumentID, data.Comment}) + record := []string{data.SignatureID, data.ProjectID, data.ReferenceID, data.ReferenceName, data.EnvelopeID, data.DocumentID, data.Comment, data.Status} + err = writer.Write(record) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem writing record to report file") + } + } + + log.WithFields(f).Debugf("report generated successfully, total: %d, success: %d, failed: %d", len(report), success, failed) + + log.WithFields(f).Debug("report generated successfully") +} diff --git a/cla-backend-go/cmd/server.go b/cla-backend-go/cmd/server.go index 6ac014d60..e2291e37d 100644 --- a/cla-backend-go/cmd/server.go +++ b/cla-backend-go/cmd/server.go @@ -6,6 +6,7 @@ package cmd import ( "encoding/json" "errors" + "fmt" "io" "net/http" "net/url" @@ -14,6 +15,20 @@ import ( "strconv" "strings" + "github.com/communitybridge/easycla/cla-backend-go/project/repository" + "github.com/communitybridge/easycla/cla-backend-go/project/service" + + gitlab_activity "github.com/communitybridge/easycla/cla-backend-go/v2/gitlab-activity" + + "github.com/go-openapi/strfmt" + + "github.com/communitybridge/easycla/cla-backend-go/v2/gitlab_organizations" + + gitlab "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" + "github.com/communitybridge/easycla/cla-backend-go/v2/gitlab_sign" + + "github.com/communitybridge/easycla/cla-backend-go/emails" + "github.com/communitybridge/easycla/cla-backend-go/v2/dynamo_events" v2GithubActivity "github.com/communitybridge/easycla/cla-backend-go/v2/github_activity" @@ -45,7 +60,7 @@ import ( lfxAuth "github.com/LF-Engineering/lfx-kit/auth" "github.com/communitybridge/easycla/cla-backend-go/docs" - "github.com/communitybridge/easycla/cla-backend-go/repositories" + v1Repositories "github.com/communitybridge/easycla/cla-backend-go/repositories" "github.com/communitybridge/easycla/cla-backend-go/utils" v2Docs "github.com/communitybridge/easycla/cla-backend-go/v2/docs" v2Events "github.com/communitybridge/easycla/cla-backend-go/v2/events" @@ -57,6 +72,7 @@ import ( "github.com/communitybridge/easycla/cla-backend-go/events" "github.com/communitybridge/easycla/cla-backend-go/project" + "github.com/communitybridge/easycla/cla-backend-go/v2/approvals" v2Project "github.com/communitybridge/easycla/cla-backend-go/v2/project" "github.com/communitybridge/easycla/cla-backend-go/users" @@ -68,11 +84,11 @@ import ( log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/auth" - "github.com/communitybridge/easycla/cla-backend-go/company" + v1Company "github.com/communitybridge/easycla/cla-backend-go/company" "github.com/communitybridge/easycla/cla-backend-go/docraptor" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations" v2RestAPI "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi" v2Ops "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations" "github.com/communitybridge/easycla/cla-backend-go/github" @@ -82,6 +98,7 @@ import ( v2ClaManager "github.com/communitybridge/easycla/cla-backend-go/v2/cla_manager" v2Company "github.com/communitybridge/easycla/cla-backend-go/v2/company" v2Health "github.com/communitybridge/easycla/cla-backend-go/v2/health" + "github.com/communitybridge/easycla/cla-backend-go/v2/store" v2Template "github.com/communitybridge/easycla/cla-backend-go/v2/template" "github.com/go-openapi/loads" @@ -120,8 +137,9 @@ func init() { type combinedRepo struct { users.UserRepository - company.IRepository - project.ProjectRepository + v1Company.IRepository + repository.ProjectRepository + projects_cla_groups.Repository } // server function called by environment specific server functions @@ -223,67 +241,81 @@ func server(localMode bool) http.Handler { if err != nil { logrus.Panic(err) } - github.Init(configFile.Github.AppID, configFile.Github.AppPrivateKey, configFile.Github.AccessToken) + // initialize github + github.Init(configFile.GitHub.AppID, configFile.GitHub.AppPrivateKey, configFile.GitHub.AccessToken) + // initialize gitlab + gitlabApp := gitlab.Init(configFile.Gitlab.AppClientID, configFile.Gitlab.AppClientSecret, configFile.Gitlab.AppPrivateKey) // Our backend repository handlers userRepo := user.NewDynamoRepository(awsSession, stage) usersRepo := users.NewRepository(awsSession, stage) - repositoriesRepo := repositories.NewRepository(awsSession, stage) + gitV1Repository := v1Repositories.NewRepository(awsSession, stage) + gitV2Repository := v2Repositories.NewRepository(awsSession, stage) gerritRepo := gerrits.NewRepository(awsSession, stage) templateRepo := template.NewRepository(awsSession, stage) approvalListRepo := approval_list.NewRepository(awsSession, stage) - companyRepo := company.NewRepository(awsSession, stage) - signaturesRepo := signatures.NewRepository(awsSession, stage, companyRepo, usersRepo) - projectClaGroupRepo := projects_cla_groups.NewRepository(awsSession, stage) - projectRepo := project.NewRepository(awsSession, stage, repositoriesRepo, gerritRepo, projectClaGroupRepo) + v1CompanyRepo := v1Company.NewRepository(awsSession, stage) eventsRepo := events.NewRepository(awsSession, stage) - metricsRepo := metrics.NewRepository(awsSession, stage, configFile.APIGatewayURL, projectClaGroupRepo) + v1ProjectClaGroupRepo := projects_cla_groups.NewRepository(awsSession, stage) + v1CLAGroupRepo := repository.NewRepository(awsSession, stage, gitV1Repository, gerritRepo, v1ProjectClaGroupRepo) + metricsRepo := metrics.NewRepository(awsSession, stage, configFile.APIGatewayURL, v1ProjectClaGroupRepo) githubOrganizationsRepo := github_organizations.NewRepository(awsSession, stage) + gitlabOrganizationRepo := gitlab_organizations.NewRepository(awsSession, stage) claManagerReqRepo := cla_manager.NewRepository(awsSession, stage) + storeRepository := store.NewRepository(awsSession, stage) + approvalsRepo := approvals.NewRepository(stage, awsSession, fmt.Sprintf("cla-%s-approvals", stage)) // Our service layer handlers eventsService := events.NewService(eventsRepo, combinedRepo{ usersRepo, - companyRepo, - projectRepo, + v1CompanyRepo, + v1CLAGroupRepo, + v1ProjectClaGroupRepo, }) + gerritService := gerrits.NewService(gerritRepo) + + // Signature repository handler + signaturesRepo := signatures.NewRepository(awsSession, stage, v1CompanyRepo, usersRepo, eventsService, gitV1Repository, githubOrganizationsRepo, gerritService, approvalsRepo) + // Initialize the external platform services - these are external APIs that // we download the swagger specification, generate the models, and have //client helper functions - user_service.InitClient(configFile.APIGatewayURL, configFile.AcsAPIKey) - project_service.InitClient(configFile.APIGatewayURL) - organization_service.InitClient(configFile.APIGatewayURL, eventsService) - acs_service.InitClient(configFile.APIGatewayURL, configFile.AcsAPIKey) + user_service.InitClient(configFile.PlatformAPIGatewayURL, configFile.AcsAPIKey) + project_service.InitClient(configFile.PlatformAPIGatewayURL) + organization_service.InitClient(configFile.PlatformAPIGatewayURL, eventsService) + acs_service.InitClient(configFile.PlatformAPIGatewayURL, configFile.AcsAPIKey) + v1ProjectClaGroupService := projects_cla_groups.NewService(v1ProjectClaGroupRepo) usersService := users.NewService(usersRepo, eventsService) healthService := health.New(Version, Commit, Branch, BuildDate) templateService := template.NewService(stage, templateRepo, docraptorClient, awsSession) - projectService := project.NewService(projectRepo, repositoriesRepo, gerritRepo, projectClaGroupRepo, usersRepo) - v2ProjectService := v2Project.NewService(projectService, projectRepo, projectClaGroupRepo) - companyService := company.NewService(companyRepo, configFile.CorporateConsoleURL, userRepo, usersService) - v2CompanyService := v2Company.NewService(companyService, signaturesRepo, projectRepo, usersRepo, companyRepo, projectClaGroupRepo, eventsService) - v2SignService := sign.NewService(configFile.ClaV1ApiURL, companyRepo, projectRepo, projectClaGroupRepo, companyService) - signaturesService := signatures.NewService(signaturesRepo, companyService, usersService, eventsService, githubOrgValidation) - v2SignatureService := v2Signatures.NewService(awsSession, configFile.SignatureFilesBucket, projectService, companyService, signaturesService, projectClaGroupRepo) - v1ClaManagerService := cla_manager.NewService(claManagerReqRepo, companyService, projectService, usersService, signaturesService, eventsService, configFile.CorporateConsoleURL) - repositoriesService := repositories.NewService(repositoriesRepo, githubOrganizationsRepo, projectClaGroupRepo) - v2RepositoriesService := v2Repositories.NewService(repositoriesRepo, projectClaGroupRepo, githubOrganizationsRepo) - v2ClaManagerService := v2ClaManager.NewService(companyService, projectService, v1ClaManagerService, usersService, repositoriesService, v2CompanyService, eventsService, projectClaGroupRepo) - approvalListService := approval_list.NewService(approvalListRepo, usersRepo, companyRepo, projectRepo, signaturesRepo, configFile.CorporateConsoleURL, http.DefaultClient) + v1ProjectService := service.NewService(v1CLAGroupRepo, gitV1Repository, gerritRepo, v1ProjectClaGroupRepo, usersRepo) + emailTemplateService := emails.NewEmailTemplateService(v1CLAGroupRepo, v1ProjectClaGroupRepo, v1ProjectService, configFile.CorporateConsoleV1URL, configFile.CorporateConsoleV2URL) + emailService := emails.NewService(emailTemplateService, v1ProjectService) + v2ProjectService := v2Project.NewService(v1ProjectService, v1CLAGroupRepo, v1ProjectClaGroupRepo) + v1CompanyService := v1Company.NewService(v1CompanyRepo, configFile.CorporateConsoleV1URL, userRepo, usersService) + v2CompanyService := v2Company.NewService(v1CompanyService, signaturesRepo, v1CLAGroupRepo, usersRepo, v1CompanyRepo, v1ProjectClaGroupRepo, eventsService) + + v1RepositoriesService := v1Repositories.NewService(gitV1Repository, githubOrganizationsRepo, v1ProjectClaGroupRepo) + v2RepositoriesService := v2Repositories.NewService(gitV1Repository, gitV2Repository, v1ProjectClaGroupRepo, githubOrganizationsRepo, gitlabOrganizationRepo, eventsService) + githubOrganizationsService := github_organizations.NewService(githubOrganizationsRepo, gitV1Repository, v1ProjectClaGroupRepo) + gitlabOrganizationsService := gitlab_organizations.NewService(gitlabOrganizationRepo, v2RepositoriesService, v1ProjectClaGroupRepo, storeRepository, usersService, signaturesRepo, v1CompanyRepo) + v1SignaturesService := signatures.NewService(signaturesRepo, v1CompanyService, usersService, eventsService, githubOrgValidation, v1RepositoriesService, githubOrganizationsService, v1ProjectService, gitlabApp, configFile.ClaV1ApiURL, configFile.CLALandingPage, configFile.CLALogoURL) + v2SignatureService := v2Signatures.NewService(awsSession, configFile.SignatureFilesBucket, v1ProjectService, v1CompanyService, v1SignaturesService, v1ProjectClaGroupRepo, signaturesRepo, usersService, approvalsRepo) + v1ClaManagerService := cla_manager.NewService(claManagerReqRepo, v1ProjectClaGroupRepo, v1CompanyService, v1ProjectService, usersService, v1SignaturesService, eventsService, emailTemplateService, configFile.CorporateConsoleV1URL) + v2ClaManagerService := v2ClaManager.NewService(emailTemplateService, v1CompanyService, v1ProjectService, v1ClaManagerService, usersService, v1RepositoriesService, v2CompanyService, eventsService, v1ProjectClaGroupRepo) + v1ApprovalListService := approval_list.NewService(approvalListRepo, v1ProjectClaGroupRepo, v1ProjectService, usersRepo, v1CompanyRepo, v1CLAGroupRepo, signaturesRepo, emailTemplateService, configFile.CorporateConsoleV2URL, http.DefaultClient) authorizer := auth.NewAuthorizer(authValidator, userRepo) - v2MetricsService := metrics.NewService(metricsRepo, projectClaGroupRepo) - githubOrganizationsService := github_organizations.NewService(githubOrganizationsRepo, repositoriesRepo, projectClaGroupRepo) - v2GithubOrganizationsService := v2GithubOrganizations.NewService(githubOrganizationsRepo, repositoriesRepo, projectClaGroupRepo) - autoEnableService := dynamo_events.NewAutoEnableService(repositoriesService, repositoriesRepo, githubOrganizationsRepo, projectClaGroupRepo, projectService) - v2GithubActivityService := v2GithubActivity.NewService(repositoriesRepo, eventsService, autoEnableService) - gerritService := gerrits.NewService(gerritRepo, &gerrits.LFGroup{ - LfBaseURL: configFile.LFGroup.ClientURL, - ClientID: configFile.LFGroup.ClientID, - ClientSecret: configFile.LFGroup.ClientSecret, - RefreshToken: configFile.LFGroup.RefreshToken, - }) - v2ClaGroupService := cla_groups.NewService(projectService, templateService, projectClaGroupRepo, v1ClaManagerService, signaturesService, metricsRepo, gerritService, repositoriesService, eventsService) + v2MetricsService := metrics.NewService(metricsRepo, v1ProjectClaGroupRepo) + gitlabActivityService := gitlab_activity.NewService(gitV1Repository, gitV2Repository, usersRepo, signaturesRepo, v1ProjectClaGroupRepo, v1CompanyRepo, signaturesRepo, gitlabOrganizationsService) + gitlabSignService := gitlab_sign.NewService(v2RepositoriesService, usersService, storeRepository, gitlabApp, gitlabOrganizationsService) + v2GithubOrganizationsService := v2GithubOrganizations.NewService(githubOrganizationsRepo, gitV1Repository, v1ProjectClaGroupRepo, githubOrganizationsService) + autoEnableService := dynamo_events.NewAutoEnableService(v1RepositoriesService, gitV1Repository, githubOrganizationsRepo, v1ProjectClaGroupRepo, v1ProjectService) + v2GithubActivityService := v2GithubActivity.NewService(gitV1Repository, githubOrganizationsRepo, eventsService, autoEnableService, emailService) + + v2ClaGroupService := cla_groups.NewService(v1ProjectService, templateService, v1ProjectClaGroupRepo, v1ClaManagerService, v1SignaturesService, metricsRepo, gerritService, v1RepositoriesService, eventsService) + v2SignService := sign.NewService(configFile.ClaAPIV4Base, configFile.ClaV1ApiURL, v1CompanyRepo, v1CLAGroupRepo, v1ProjectClaGroupRepo, v1CompanyService, v2ClaGroupService, configFile.DocuSignPrivateKey, usersService, v1SignaturesService, storeRepository, v1RepositoriesService, githubOrganizationsService, gitlabOrganizationsService, configFile.CLALandingPage, configFile.CLALogoURL, emailService, eventsService, gitlabActivityService, gitlabApp, gerritService) sessionStore, err := dynastore.New(dynastore.Path("/"), dynastore.HTTPOnly(), dynastore.TableName(configFile.SessionStoreTableName), dynastore.DynamoDB(dynamodb.New(awsSession))) if err != nil { @@ -298,37 +330,45 @@ func server(localMode bool) http.Handler { // Setup our API handlers users.Configure(api, usersService, eventsService) - project.Configure(api, projectService, eventsService, gerritService, repositoriesService, signaturesService) - v2Project.Configure(v2API, projectService, v2ProjectService, eventsService) + project.Configure(api, v1ProjectService, eventsService, gerritService, v1RepositoriesService, v1SignaturesService) + v2Project.Configure(v2API, v1ProjectService, v2ProjectService, eventsService) health.Configure(api, healthService) v2Health.Configure(v2API, healthService) template.Configure(api, templateService, eventsService) - v2Template.Configure(v2API, templateService, eventsService) - github.Configure(api, configFile.Github.ClientID, configFile.Github.ClientSecret, configFile.Github.AccessToken, sessionStore) - signatures.Configure(api, signaturesService, sessionStore, eventsService) - v2Signatures.Configure(v2API, projectService, projectRepo, companyService, signaturesService, sessionStore, eventsService, v2SignatureService, projectClaGroupRepo) - approval_list.Configure(api, approvalListService, sessionStore, signaturesService, eventsService) - company.Configure(api, companyService, usersService, companyUserValidation, eventsService) + v2Template.Configure(v2API, templateService, v1ProjectClaGroupService, eventsService) + github.Configure(api, configFile.GitHub.ClientID, configFile.GitHub.ClientSecret, configFile.GitHub.AccessToken, sessionStore) + signatures.Configure(api, v1SignaturesService, sessionStore, eventsService) + v2Signatures.Configure(v2API, v1ProjectService, v1CLAGroupRepo, v1CompanyService, v1SignaturesService, sessionStore, eventsService, v2SignatureService, v1ProjectClaGroupRepo) + approval_list.Configure(api, v1ApprovalListService, sessionStore, v1SignaturesService, eventsService) + v1Company.Configure(api, v1CompanyService, usersService, companyUserValidation, eventsService) docs.Configure(api) v2Docs.Configure(v2API) version.Configure(api, Version, Commit, Branch, BuildDate) v2Version.Configure(v2API, Version, Commit, Branch, BuildDate) events.Configure(api, eventsService) - v2Events.Configure(v2API, eventsService, companyRepo, projectClaGroupRepo) - v2Metrics.Configure(v2API, v2MetricsService, companyRepo) + v2Events.Configure(v2API, eventsService, v1CompanyRepo, v1ProjectClaGroupRepo, v1ProjectService) + v2Metrics.Configure(v2API, v2MetricsService, v1CompanyRepo) github_organizations.Configure(api, githubOrganizationsService, eventsService) v2GithubOrganizations.Configure(v2API, v2GithubOrganizationsService, eventsService) - repositories.Configure(api, repositoriesService, eventsService) + gitlab_organizations.Configure(v2API, gitlabOrganizationsService, eventsService, sessionStore, configFile.CLAContributorv2Base) + gitlab_sign.Configure(v2API, gitlabSignService, eventsService, configFile.CLAContributorv2Base, sessionStore) + gitlab_activity.Configure(v2API, gitlabActivityService, gitlabOrganizationsService, eventsService, gitlabApp, gitlabSignService, configFile.CLAContributorv2Base, sessionStore) + v1Repositories.Configure(api, v1RepositoriesService, eventsService) v2Repositories.Configure(v2API, v2RepositoriesService, eventsService) - gerrits.Configure(api, gerritService, projectService, eventsService) - v2Gerrits.Configure(v2API, gerritService, projectService, eventsService, projectClaGroupRepo) - v2Company.Configure(v2API, v2CompanyService, companyRepo, projectClaGroupRepo, configFile.LFXPortalURL, configFile.CorporateConsoleURL) - cla_manager.Configure(api, v1ClaManagerService, companyService, projectService, usersService, signaturesService, eventsService, configFile.CorporateConsoleURL) - v2ClaManager.Configure(v2API, v2ClaManagerService, configFile.LFXPortalURL, projectClaGroupRepo, userRepo) - sign.Configure(v2API, v2SignService) - cla_groups.Configure(v2API, v2ClaGroupService, projectService, projectClaGroupRepo, eventsService) + gerrits.Configure(api, gerritService, v1ProjectService, eventsService) + v2Gerrits.Configure(v2API, gerritService, v1ProjectService, eventsService, v1ProjectClaGroupRepo) + v2Company.Configure(v2API, v2CompanyService, v1ProjectClaGroupRepo, configFile.LFXPortalURL, configFile.CorporateConsoleV1URL) + cla_manager.Configure(api, v1ClaManagerService, v1CompanyService, v1ProjectService, usersService, v1SignaturesService, eventsService, emailTemplateService) + v2ClaManager.Configure(v2API, v2ClaManagerService, v1CompanyService, configFile.LFXPortalURL, configFile.CorporateConsoleV2URL, v1ProjectClaGroupRepo, userRepo) + cla_groups.Configure(v2API, v2ClaGroupService, v1ProjectService, v1ProjectClaGroupRepo, eventsService) + sign.Configure(v2API, v2SignService, usersService) v2GithubActivity.Configure(v2API, v2GithubActivityService) + v2API.AddMiddlewareFor("POST", "/signed/individual/{installation_id}/{github_repository_id}/{change_request_id}", sign.DocusignMiddleware) + v2API.AddMiddlewareFor("POST", "/signed/corporate/{project_id}/{company_id}", sign.CCLADocusignMiddleware) + v2API.AddMiddlewareFor("POST", "/signed/gitlab/individual/{user_id}/{organization_id}/{gitlab_repository_id}/{merge_request_id}", sign.DocusignMiddleware) + v2API.AddMiddlewareFor("POST", "/signed/gerrit/individual/{user_id}", sign.DocusignMiddleware) + userCreaterMiddleware := func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { createUserFromRequest(authorizer, usersService, eventsService, r) @@ -591,13 +631,23 @@ func createUserFromRequest(authorizer auth.Authorizer, usersService users.Servic log.WithFields(f).WithError(err).Warn("parsing failed") return } + f["claUserName"] = claUser.Name + f["claUserID"] = claUser.UserID + f["claUserLFUsername"] = claUser.LFUsername + f["claUserLFEmail"] = claUser.LFEmail + f["claUserEmails"] = strings.Join(claUser.Emails, ",") // search if user exist in database by username userModel, err := usersService.GetUserByLFUserName(claUser.LFUsername) if err != nil { - log.WithFields(f).WithError(err).Warn("searching user by lf-username failed") - return + if _, ok := err.(*utils.UserNotFound); ok { + log.WithFields(f).Debug("unable to locate user by lf-email") + } else { + log.WithFields(f).WithError(err).Warn("searching user by lf-username failed") + return + } } + // If found - just return if userModel != nil { return } @@ -605,20 +655,25 @@ func createUserFromRequest(authorizer auth.Authorizer, usersService users.Servic // search if user exist in database by username userModel, err = usersService.GetUserByEmail(claUser.LFEmail) if err != nil { - log.WithFields(f).WithError(err).Warn("searching user by lf-email failed") - return + if _, ok := err.(*utils.UserNotFound); ok { + log.WithFields(f).Debug("unable to locate user by lf-email") + } else { + log.WithFields(f).WithError(err).Warn("searching user by lf-email failed") + return + } } + // If found - just return if userModel != nil { return } // Attempt to create the user newUser := &models.User{ - LfEmail: claUser.LFEmail, + LfEmail: strfmt.Email(claUser.LFEmail), LfUsername: claUser.LFUsername, Username: claUser.Name, } - log.WithFields(f).WithField("user", newUser).Debug("creating new user") + log.WithFields(f).Debug("creating new user") userModel, err = usersService.CreateUser(newUser, nil) if err != nil { log.WithFields(f).WithField("user", newUser).WithError(err).Warn("creating new user failed") diff --git a/cla-backend-go/cmd/server_aws_lambda.go b/cla-backend-go/cmd/server_aws_lambda.go index 65a0dea80..2e473f0c2 100644 --- a/cla-backend-go/cmd/server_aws_lambda.go +++ b/cla-backend-go/cmd/server_aws_lambda.go @@ -1,3 +1,4 @@ +//go:build aws_lambda // +build aws_lambda // Copyright The Linux Foundation and each contributor to CommunityBridge. diff --git a/cla-backend-go/cmd/server_standalone.go b/cla-backend-go/cmd/server_standalone.go index fe2e193a1..46897cc78 100644 --- a/cla-backend-go/cmd/server_standalone.go +++ b/cla-backend-go/cmd/server_standalone.go @@ -1,3 +1,4 @@ +//go:build !aws_lambda // +build !aws_lambda // Copyright The Linux Foundation and each contributor to CommunityBridge. @@ -26,7 +27,7 @@ func runServer(cmd *cobra.Command, args []string) { errs := make(chan error, 2) go func() { log.Infof("Running http server on port: %d - set PORT environment variable to change port", viper.GetInt("PORT")) - errs <- http.ListenAndServe(fmt.Sprintf(":%d", viper.GetInt("PORT")), handler) + errs <- http.ListenAndServe(fmt.Sprintf(":%d", viper.GetInt("PORT")), handler) // nolint gosec no support for setting timeouts }() go func() { c := make(chan os.Signal) diff --git a/cla-backend-go/userSubscribeLambda/cmd/serve_lambda.go b/cla-backend-go/cmd/user-subscribe-lambda/cmd/serve_lambda.go similarity index 95% rename from cla-backend-go/userSubscribeLambda/cmd/serve_lambda.go rename to cla-backend-go/cmd/user-subscribe-lambda/cmd/serve_lambda.go index 31c44a848..e5a61ff21 100644 --- a/cla-backend-go/userSubscribeLambda/cmd/serve_lambda.go +++ b/cla-backend-go/cmd/user-subscribe-lambda/cmd/serve_lambda.go @@ -1,3 +1,4 @@ +//go:build aws_lambda // +build aws_lambda // Copyright The Linux Foundation and each contributor to CommunityBridge. diff --git a/cla-backend-go/userSubscribeLambda/cmd/serve_local.go b/cla-backend-go/cmd/user-subscribe-lambda/cmd/serve_local.go similarity index 90% rename from cla-backend-go/userSubscribeLambda/cmd/serve_local.go rename to cla-backend-go/cmd/user-subscribe-lambda/cmd/serve_local.go index 8d15e4e64..220fe39f8 100644 --- a/cla-backend-go/userSubscribeLambda/cmd/serve_local.go +++ b/cla-backend-go/cmd/user-subscribe-lambda/cmd/serve_local.go @@ -1,3 +1,4 @@ +//go:build !aws_lambda // +build !aws_lambda // Copyright The Linux Foundation and each contributor to CommunityBridge. @@ -8,7 +9,7 @@ package cmd import ( "context" "fmt" - "io/ioutil" + "io" "net/http" "time" @@ -23,7 +24,7 @@ func postSQSEvent(w http.ResponseWriter, r *http.Request) { return } - dataByte, err := ioutil.ReadAll(r.Body) + dataByte, err := io.ReadAll(r.Body) if err != nil { log.Println("Failed to read body") http.Error(w, "Failed to read body", http.StatusInternalServerError) @@ -66,7 +67,7 @@ func Start(hf fn) error { http.HandleFunc("/", postSQSEvent) fmt.Printf("Starting server for testing HTTP POST...\n") - if err := http.ListenAndServe(":8080", nil); err != nil { + if err := http.ListenAndServe(":8080", nil); err != nil { // nolint gosec http no support for setting timeouts log.Fatal(err) } return nil diff --git a/cla-backend-go/cmd/user-subscribe-lambda/main.go b/cla-backend-go/cmd/user-subscribe-lambda/main.go new file mode 100644 index 000000000..08dec8174 --- /dev/null +++ b/cla-backend-go/cmd/user-subscribe-lambda/main.go @@ -0,0 +1,350 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "fmt" + "os" + "runtime" + + "github.com/communitybridge/easycla/cla-backend-go/cmd/user-subscribe-lambda/cmd" + + "github.com/go-openapi/strfmt" + + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/sirupsen/logrus" + + "github.com/LF-Engineering/lfx-models/models/event" + usersModels "github.com/LF-Engineering/lfx-models/models/users" + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/communitybridge/easycla/cla-backend-go/config" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/token" + "github.com/communitybridge/easycla/cla-backend-go/users" + user_service "github.com/communitybridge/easycla/cla-backend-go/v2/user-service" + "github.com/mitchellh/mapstructure" +) + +// Build and version variables defined and set during the build process +var ( + // version the application version + version string + + // build/Commit the application build number + commit string + + // build date + buildDate string +) + +func init() { + f := logrus.Fields{ + "functionName": "userSubscribeLambda.main.init", + } + var awsSession = session.Must(session.NewSession(&aws.Config{})) + stage := os.Getenv("STAGE") + if stage == "" { + log.WithFields(f).Fatal("stage not set") + } + log.WithFields(f).Infof("STAGE set to %s\n", stage) + configFile, err := config.LoadConfig("", awsSession, stage) + if err != nil { + log.WithFields(f).WithError(err).Panicf("Unable to load config - Error: %v", err) + } + + token.Init(configFile.Auth0Platform.ClientID, configFile.Auth0Platform.ClientSecret, configFile.Auth0Platform.URL, configFile.Auth0Platform.Audience) + user_service.InitClient(configFile.APIGatewayURL, configFile.AcsAPIKey) +} + +// Handler is the user subscribe handler lambda entry function +func Handler(ctx context.Context, snsEvent events.SNSEvent) error { + f := logrus.Fields{ + "functionName": "userSubscribeLambda.main.Handler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + if len(snsEvent.Records) == 0 { + log.WithFields(f).Warn("SNS event contained 0 records - ignoring message.") + return nil + } + + for _, message := range snsEvent.Records { + log.WithFields(f).Infof("Processing message id: '%s' for event source '%s'", message.SNS.MessageID, message.EventSource) + + log.WithFields(f).Debugf("Unmarshalling message body: '%s'", message.SNS.Message) + var model event.Event + err := model.UnmarshalBinary([]byte(message.SNS.Message)) + if err != nil { + log.WithFields(f).Warnf("Error: %v, JSON unmarshal failed - unable to process message: %s", err, message.SNS.MessageID) + return err + } + + f["modelType"] = model.Type + log.WithFields(f).Debugf("Processing message type: %s", model.Type) + switch model.Type { + case "UserSignedUp": + log.WithFields(f).Debugf("Detected message type: %s - processing...", model.Type) + Create(ctx, model) + case "UserUpdatedProfile": + log.WithFields(f).Debugf("Detected message type: %s - processing...", model.Type) + Update(ctx, model) + case "UserAuthenticated": + log.WithFields(f).Debugf("Ignoring message type: %s", model.Type) + default: + log.WithFields(f).Warnf("unrecognized message type: %s - unable to process message ", model.Type) + } + + } + return nil +} + +// Create saves the user data model to persistent storage +func Create(ctx context.Context, user event.Event) { + f := logrus.Fields{ + "functionName": "userSubscribeLambda.main.Create", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + uc := &usersModels.UserCreated{} + err := mapstructure.Decode(user.Data, uc) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to decode event") + return + } + + var userDetails *models.User + var userErr error + var awsSession = session.Must(session.NewSession(&aws.Config{})) + + stage := os.Getenv("STAGE") + if stage == "" { + log.Fatal("stage not set") + } + usersRepo := users.NewRepository(awsSession, stage) + + log.WithFields(f).Debugf("locating user by username: %s in EasyCLA's database...", uc.Username) + userDetails, userErr = usersRepo.GetUserByLFUserName(uc.Username) + if userErr != nil { + log.WithFields(f).WithError(userErr).Warnf("unable to locate user by LfUsername: %s", uc.Username) + } + + if userDetails != nil { + log.WithFields(f).Warnf("unable to create user - user already created: %s", uc.Username) + } + + userServiceClient := user_service.GetClient() + log.WithFields(f).Debugf("locating user by username: %s in the user service...", uc.Username) + sfdcUserObject, err := userServiceClient.GetUserByUsername(uc.Username) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to locate user by username: %s", uc.Username) + return + } + if sfdcUserObject == nil { + log.WithFields(f).Debugf("User-service model is nil so skipping user %s", uc.Username) + return + } + + log.WithFields(f).Debugf("Salesforce user-service object : %+v", sfdcUserObject) + + var primaryEmail string + var emails []string + for _, email := range sfdcUserObject.Emails { + if *email.IsPrimary { + primaryEmail = *email.EmailAddress + } + emails = append(emails, *email.EmailAddress) + } + + _, nowStr := utils.CurrentTime() + createUserModel := &models.User{ + Admin: false, + DateCreated: nowStr, + DateModified: nowStr, + Emails: emails, + LfEmail: strfmt.Email(primaryEmail), + LfUsername: sfdcUserObject.Username, + Note: "Create via user-service event", + UserExternalID: sfdcUserObject.ID, + UserID: userDetails.UserID, + Username: fmt.Sprintf("%s %s", sfdcUserObject.FirstName, sfdcUserObject.LastName), + Version: "v1", + } + + log.WithFields(f).Debugf("Creating user in Dynamo DB : %+v", createUserModel) + _, createErr := usersRepo.CreateUser(createUserModel) + if createErr != nil { + log.WithFields(f).Warnf("unable to create user by LfUsername: %s", uc.Username) + return + } +} + +// Update saves the user data model to persistent storage +func Update(ctx context.Context, user event.Event) { + f := logrus.Fields{ + "functionName": "userSubscribeLambda.main.Update", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + uc := &usersModels.UserUpdated{} + err := mapstructure.Decode(user.Data, uc) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to decode event") + return + } + + var userDetails *models.User + var userErr error + var awsSession = session.Must(session.NewSession(&aws.Config{})) + + stage := os.Getenv("STAGE") + if stage == "" { + log.Fatal("stage not set") + } + usersRepo := users.NewRepository(awsSession, stage) + + userDetails, userErr = usersRepo.GetUserByLFUserName(*uc.Username) + if userErr != nil { + log.WithFields(f).WithError(userErr).Warnf("unable to locate user by LfUsername: %s", *uc.Username) + } + + if userDetails == nil { + for _, email := range uc.Emails { + userDetails, userErr = usersRepo.GetUserByEmail(*email.EmailAddress) + if userErr != nil { + log.WithFields(f).WithError(userErr).Warnf("unable to locate user by LfUsername: %s", *uc.Username) + } + } + } + + if userDetails == nil { + userDetails, userErr = usersRepo.GetUserByExternalID(uc.UserID) + if userErr != nil { + log.WithFields(f).WithError(userErr).Warnf("unable to locate user by UserExternalID: %s", uc.UserID) + } + } + + if userDetails == nil { + log.WithFields(f).Debugf("User model is nil - adding as new user %s...", *uc.Username) + // Attempt to create the user from the upate model + createFromUpdateErr := createUserFromUpdatedModel(uc) + if createFromUpdateErr != nil { + log.WithFields(f).WithError(createFromUpdateErr).Warnf("unable to create new user record from user service update message: %s", uc.UserID) + } + return + } + + userServiceClient := user_service.GetClient() + sfdcUserObject, err := userServiceClient.GetUser(uc.UserID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to locate user by SFID: %s, error: %+v", uc.UserID, userErr) + return + } + + log.WithFields(f).Debugf("Salesforce user-service object : %+v", sfdcUserObject) + + if sfdcUserObject == nil { + log.WithFields(f).Debugf("User-service model is nil so skipping user %s with SFID %s", *uc.Username, uc.UserID) + return + } + + var primaryEmail string + var emails []string + for _, email := range sfdcUserObject.Emails { + if *email.IsPrimary { + primaryEmail = *email.EmailAddress + } + emails = append(emails, *email.EmailAddress) + } + + updateUserModel := &models.UserUpdate{ + Emails: emails, + LfEmail: primaryEmail, + LfUsername: sfdcUserObject.Username, + Note: "Update via user-service event", + UserExternalID: sfdcUserObject.ID, + UserID: userDetails.UserID, + Username: fmt.Sprintf("%s %s", sfdcUserObject.FirstName, sfdcUserObject.LastName), + } + + log.WithFields(f).Debugf("Updating user in Dynamo DB : %+v", updateUserModel) + _, updateErr := usersRepo.Save(updateUserModel) + if updateErr != nil { + log.WithFields(f).Warnf("Error - unable to update user by LfUsername: %s, error: %+v", *uc.Username, updateErr) + return + } +} + +func createUserFromUpdatedModel(userModelUpdated *usersModels.UserUpdated) error { + f := logrus.Fields{ + "functionName": "userSubscribeLambda.main.createUserFromUpdatedModel", + "userID": userModelUpdated.UserID, + "userName": userModelUpdated.Username, + } + + var awsSession = session.Must(session.NewSession(&aws.Config{})) + + stage := os.Getenv("STAGE") + if stage == "" { + log.Fatal("stage not set") + } + userServiceClient := user_service.GetClient() + sfdcUserObject, err := userServiceClient.GetUser(userModelUpdated.UserID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to locate user by ID: %s", userModelUpdated.UserID) + return err + } + + var primaryEmail string + var emails []string + for _, email := range sfdcUserObject.Emails { + if *email.IsPrimary { + primaryEmail = *email.EmailAddress + } + emails = append(emails, *email.EmailAddress) + } + + newUserModel := &models.User{ + Emails: emails, + LfEmail: strfmt.Email(primaryEmail), + LfUsername: sfdcUserObject.Username, + Note: "Update via user-service event", + UserExternalID: sfdcUserObject.ID, + UserID: userModelUpdated.UserID, + Username: fmt.Sprintf("%s %s", sfdcUserObject.FirstName, sfdcUserObject.LastName), + } + + log.WithFields(f).Debugf("Creating user in Dynamo DB : %+v", newUserModel) + usersRepo := users.NewRepository(awsSession, stage) + + _, createErr := usersRepo.CreateUser(newUserModel) + if createErr != nil { + log.WithFields(f).WithError(createErr).Warnf("unable to create user by LfUsername: %s", *userModelUpdated.Username) + return createErr + } + + return nil +} + +func main() { + f := logrus.Fields{ + "functionName": "userSubscribeLambda.main.main", + } + var err error + + // Show the version and build info + log.WithFields(f).Infof("Name : userSubscribe handler") + log.WithFields(f).Infof("Version : %s", version) + log.WithFields(f).Infof("Git commit hash : %s", commit) + log.WithFields(f).Infof("Build date : %s", buildDate) + log.WithFields(f).Infof("Golang OS : %s", runtime.GOOS) + log.WithFields(f).Infof("Golang Arch : %s", runtime.GOARCH) + + err = cmd.Start(Handler) + if err != nil { + log.WithFields(f).WithError(err).Fatal(err) + } +} diff --git a/cla-backend-go/cmd/zipbuilder_lambda/main.go b/cla-backend-go/cmd/zipbuilder_lambda/main.go index cb0bc1375..0aed91412 100644 --- a/cla-backend-go/cmd/zipbuilder_lambda/main.go +++ b/cla-backend-go/cmd/zipbuilder_lambda/main.go @@ -36,6 +36,7 @@ var ( type BuildZipEvent struct { ClaGroupID string `json:"cla_group_id"` SignatureType string `json:"signature_type"` + FileType string `json:"file_type"` } var zipBuilder signatures.ZipBuilder @@ -59,12 +60,30 @@ func handler(ctx context.Context, event BuildZipEvent) error { var err error log.WithField("event", event).Debug("zip builder called") switch event.SignatureType { - case signatures.ICLA: - err = zipBuilder.BuildICLAZip(event.ClaGroupID) - case signatures.CCLA: - err = zipBuilder.BuildCCLAZip(event.ClaGroupID) + case utils.ClaTypeICLA: + if event.FileType == utils.FileTypePDF { + err = zipBuilder.BuildICLAPDFZip(event.ClaGroupID) + } else if event.FileType == utils.FileTypeCSV { + err = zipBuilder.BuildICLACSVZip(event.ClaGroupID) + } else { + log.WithField("event", event).Warn("Invalid event") + } + case utils.ClaTypeCCLA: + if event.FileType == utils.FileTypePDF { + err = zipBuilder.BuildCCLAPDFZip(event.ClaGroupID) + } else if event.FileType == utils.FileTypeCSV { + err = zipBuilder.BuildCCLACSVZip(event.ClaGroupID) + } else { + log.WithField("event", event).Warn("Invalid event") + } + case utils.ClaTypeECLA: + if event.FileType == utils.FileTypeCSV { + err = zipBuilder.BuildECLACSVZip(event.ClaGroupID) + } else { + log.WithField("event", event).Warn("Invalid event") + } default: - log.WithField("event", event).Debug("Invalid event") + log.WithField("event", event).Warn("Invalid event") } if err != nil { log.WithField("args", event).Error("failed to build zip", err) diff --git a/cla-backend-go/cmd/zipbuilder_scheduler_lambda/main.go b/cla-backend-go/cmd/zipbuilder_scheduler_lambda/main.go index c15063889..2229ea736 100644 --- a/cla-backend-go/cmd/zipbuilder_scheduler_lambda/main.go +++ b/cla-backend-go/cmd/zipbuilder_scheduler_lambda/main.go @@ -52,6 +52,7 @@ type ClaGroup struct { type BuildZipEvent struct { ClaGroupID string `json:"cla_group_id"` SignatureType string `json:"signature_type"` + FileType string `json:"file_type"` } func handler(ctx context.Context, event events.CloudWatchEvent) { @@ -71,13 +72,30 @@ func handler(ctx context.Context, event events.CloudWatchEvent) { if claGroup.ProjectCclaEnabled { eventPayloads = append(eventPayloads, BuildZipEvent{ ClaGroupID: claGroup.ProjectID, - SignatureType: "ccla", + SignatureType: utils.ClaTypeCCLA, + FileType: utils.FileTypePDF, + }) + eventPayloads = append(eventPayloads, BuildZipEvent{ + ClaGroupID: claGroup.ProjectID, + SignatureType: utils.ClaTypeCCLA, + FileType: utils.FileTypeCSV, + }) + eventPayloads = append(eventPayloads, BuildZipEvent{ + ClaGroupID: claGroup.ProjectID, + SignatureType: utils.ClaTypeECLA, + FileType: utils.FileTypeCSV, }) } if claGroup.ProjectIclaEnabled { eventPayloads = append(eventPayloads, BuildZipEvent{ ClaGroupID: claGroup.ProjectID, - SignatureType: "icla", + SignatureType: utils.ClaTypeICLA, + FileType: utils.FileTypePDF, + }) + eventPayloads = append(eventPayloads, BuildZipEvent{ + ClaGroupID: claGroup.ProjectID, + SignatureType: utils.ClaTypeICLA, + FileType: utils.FileTypeCSV, }) } } diff --git a/cla-backend-go/company/handlers.go b/cla-backend-go/company/handlers.go index 1085b5ed6..f7216dd86 100644 --- a/cla-backend-go/company/handlers.go +++ b/cla-backend-go/company/handlers.go @@ -12,14 +12,14 @@ import ( "github.com/communitybridge/easycla/cla-backend-go/utils" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/organization" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/organization" "github.com/communitybridge/easycla/cla-backend-go/events" "github.com/communitybridge/easycla/cla-backend-go/users" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/company" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/company" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/user" orgService "github.com/communitybridge/easycla/cla-backend-go/v2/organization-service" @@ -257,7 +257,7 @@ func Configure(api *operations.ClaAPI, service IService, usersService users.Serv return company.NewAddUsertoCompanyAccessListBadRequest().WithXRequestID(reqID) } - eventsService.LogEvent(&events.LogEventArgs{ + eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.CompanyACLUserAdded, CompanyID: params.CompanyID, UserID: claUser.UserID, @@ -280,7 +280,7 @@ func Configure(api *operations.ClaAPI, service IService, usersService users.Serv } // Add an event to the log - eventsService.LogEvent(&events.LogEventArgs{ + eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.CompanyACLRequestAdded, CompanyID: params.CompanyID, UserID: claUser.UserID, @@ -305,7 +305,7 @@ func Configure(api *operations.ClaAPI, service IService, usersService users.Serv } // Add an event to the log - eventsService.LogEvent(&events.LogEventArgs{ + eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.CompanyACLRequestApproved, CompanyID: params.CompanyID, UserID: claUser.UserID, @@ -330,7 +330,7 @@ func Configure(api *operations.ClaAPI, service IService, usersService users.Serv } // Add an event to the log - eventsService.LogEvent(&events.LogEventArgs{ + eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.CompanyACLRequestDenied, CompanyID: params.CompanyID, UserID: claUser.UserID, @@ -347,17 +347,23 @@ func Configure(api *operations.ClaAPI, service IService, usersService users.Serv api.OrganizationSearchOrganizationHandler = organization.SearchOrganizationHandlerFunc(func(params organization.SearchOrganizationParams) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + f := logrus.Fields{ + "functionName": "company.handler.OrganizationSearchOrganizationHandler", + "companyName": params.CompanyName, + "websiteName": params.WebsiteName, + "includeSigningEntityName": params.IncludeSigningEntityName, + } if params.CompanyName == nil && params.WebsiteName == nil && params.DollarFilter == nil { - log.Debugf("CompanyName or WebsiteName or filter atleast one required") + log.WithFields(f).Debugf("CompanyName or WebsiteName or filter at least one required") return organization.NewSearchOrganizationBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(errors.New("companyName or websiteName or filter at least one required"))) } companyName, websiteName, filter := validateParams(params) - result, err := service.SearchOrganizationByName(ctx, companyName, websiteName, filter) + result, err := service.SearchOrganizationByName(ctx, companyName, websiteName, utils.BoolValue(params.IncludeSigningEntityName), filter) if err != nil { - log.Warnf("error occured while search org %s. error = %s", *params.CompanyName, err.Error()) + log.Warnf("error occurred while search org %s. error = %s", *params.CompanyName, err.Error()) return organization.NewSearchOrganizationInternalServerError().WithXRequestID(reqID).WithPayload(errorResponse(err)) } return organization.NewSearchOrganizationOK().WithXRequestID(reqID).WithPayload(result) diff --git a/cla-backend-go/company/mocks/mock_repo.go b/cla-backend-go/company/mocks/mock_repo.go new file mode 100644 index 000000000..a9a2421ff --- /dev/null +++ b/cla-backend-go/company/mocks/mock_repo.go @@ -0,0 +1,351 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +// Code generated by MockGen. DO NOT EDIT. +// Source: company/repository.go + +// Package mock_company is a generated GoMock package. +package mock_company + +import ( + context "context" + reflect "reflect" + + company "github.com/communitybridge/easycla/cla-backend-go/company" + models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + user "github.com/communitybridge/easycla/cla-backend-go/user" + gomock "github.com/golang/mock/gomock" +) + +// MockIRepository is a mock of IRepository interface. +type MockIRepository struct { + ctrl *gomock.Controller + recorder *MockIRepositoryMockRecorder +} + +// MockIRepositoryMockRecorder is the mock recorder for MockIRepository. +type MockIRepositoryMockRecorder struct { + mock *MockIRepository +} + +// NewMockIRepository creates a new mock instance. +func NewMockIRepository(ctrl *gomock.Controller) *MockIRepository { + mock := &MockIRepository{ctrl: ctrl} + mock.recorder = &MockIRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIRepository) EXPECT() *MockIRepositoryMockRecorder { + return m.recorder +} + +// AddPendingCompanyInviteRequest mocks base method. +func (m *MockIRepository) AddPendingCompanyInviteRequest(ctx context.Context, companyID string, userModel user.User) (*company.Invite, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddPendingCompanyInviteRequest", ctx, companyID, userModel) + ret0, _ := ret[0].(*company.Invite) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddPendingCompanyInviteRequest indicates an expected call of AddPendingCompanyInviteRequest. +func (mr *MockIRepositoryMockRecorder) AddPendingCompanyInviteRequest(ctx, companyID, userModel interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPendingCompanyInviteRequest", reflect.TypeOf((*MockIRepository)(nil).AddPendingCompanyInviteRequest), ctx, companyID, userModel) +} + +// ApproveCompanyAccessRequest mocks base method. +func (m *MockIRepository) ApproveCompanyAccessRequest(ctx context.Context, companyInviteID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ApproveCompanyAccessRequest", ctx, companyInviteID) + ret0, _ := ret[0].(error) + return ret0 +} + +// ApproveCompanyAccessRequest indicates an expected call of ApproveCompanyAccessRequest. +func (mr *MockIRepositoryMockRecorder) ApproveCompanyAccessRequest(ctx, companyInviteID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApproveCompanyAccessRequest", reflect.TypeOf((*MockIRepository)(nil).ApproveCompanyAccessRequest), ctx, companyInviteID) +} + +// CreateCompany mocks base method. +func (m *MockIRepository) CreateCompany(ctx context.Context, in *models.Company) (*models.Company, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateCompany", ctx, in) + ret0, _ := ret[0].(*models.Company) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateCompany indicates an expected call of CreateCompany. +func (mr *MockIRepositoryMockRecorder) CreateCompany(ctx, in interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCompany", reflect.TypeOf((*MockIRepository)(nil).CreateCompany), ctx, in) +} + +// DeleteCompanyByID mocks base method. +func (m *MockIRepository) DeleteCompanyByID(ctx context.Context, companyID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteCompanyByID", ctx, companyID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteCompanyByID indicates an expected call of DeleteCompanyByID. +func (mr *MockIRepositoryMockRecorder) DeleteCompanyByID(ctx, companyID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCompanyByID", reflect.TypeOf((*MockIRepository)(nil).DeleteCompanyByID), ctx, companyID) +} + +// DeleteCompanyBySFID mocks base method. +func (m *MockIRepository) DeleteCompanyBySFID(ctx context.Context, companySFID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteCompanyBySFID", ctx, companySFID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteCompanyBySFID indicates an expected call of DeleteCompanyBySFID. +func (mr *MockIRepositoryMockRecorder) DeleteCompanyBySFID(ctx, companySFID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCompanyBySFID", reflect.TypeOf((*MockIRepository)(nil).DeleteCompanyBySFID), ctx, companySFID) +} + +// GetCompanies mocks base method. +func (m *MockIRepository) GetCompanies(ctx context.Context) (*models.Companies, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanies", ctx) + ret0, _ := ret[0].(*models.Companies) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanies indicates an expected call of GetCompanies. +func (mr *MockIRepositoryMockRecorder) GetCompanies(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanies", reflect.TypeOf((*MockIRepository)(nil).GetCompanies), ctx) +} + +// GetCompaniesByExternalID mocks base method. +func (m *MockIRepository) GetCompaniesByExternalID(ctx context.Context, companySFID string, includeChildCompanies bool) ([]*models.Company, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompaniesByExternalID", ctx, companySFID, includeChildCompanies) + ret0, _ := ret[0].([]*models.Company) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompaniesByExternalID indicates an expected call of GetCompaniesByExternalID. +func (mr *MockIRepositoryMockRecorder) GetCompaniesByExternalID(ctx, companySFID, includeChildCompanies interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompaniesByExternalID", reflect.TypeOf((*MockIRepository)(nil).GetCompaniesByExternalID), ctx, companySFID, includeChildCompanies) +} + +// GetCompaniesByUserManager mocks base method. +func (m *MockIRepository) GetCompaniesByUserManager(ctx context.Context, userID string, userModel user.User) (*models.Companies, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompaniesByUserManager", ctx, userID, userModel) + ret0, _ := ret[0].(*models.Companies) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompaniesByUserManager indicates an expected call of GetCompaniesByUserManager. +func (mr *MockIRepositoryMockRecorder) GetCompaniesByUserManager(ctx, userID, userModel interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompaniesByUserManager", reflect.TypeOf((*MockIRepository)(nil).GetCompaniesByUserManager), ctx, userID, userModel) +} + +// GetCompaniesByUserManagerWithInvites mocks base method. +func (m *MockIRepository) GetCompaniesByUserManagerWithInvites(ctx context.Context, userID string, userModel user.User) (*models.CompaniesWithInvites, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompaniesByUserManagerWithInvites", ctx, userID, userModel) + ret0, _ := ret[0].(*models.CompaniesWithInvites) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompaniesByUserManagerWithInvites indicates an expected call of GetCompaniesByUserManagerWithInvites. +func (mr *MockIRepositoryMockRecorder) GetCompaniesByUserManagerWithInvites(ctx, userID, userModel interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompaniesByUserManagerWithInvites", reflect.TypeOf((*MockIRepository)(nil).GetCompaniesByUserManagerWithInvites), ctx, userID, userModel) +} + +// GetCompany mocks base method. +func (m *MockIRepository) GetCompany(ctx context.Context, companyID string) (*models.Company, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompany", ctx, companyID) + ret0, _ := ret[0].(*models.Company) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompany indicates an expected call of GetCompany. +func (mr *MockIRepositoryMockRecorder) GetCompany(ctx, companyID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompany", reflect.TypeOf((*MockIRepository)(nil).GetCompany), ctx, companyID) +} + +// GetCompanyByExternalID mocks base method. +func (m *MockIRepository) GetCompanyByExternalID(ctx context.Context, companySFID string) (*models.Company, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanyByExternalID", ctx, companySFID) + ret0, _ := ret[0].(*models.Company) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanyByExternalID indicates an expected call of GetCompanyByExternalID. +func (mr *MockIRepositoryMockRecorder) GetCompanyByExternalID(ctx, companySFID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanyByExternalID", reflect.TypeOf((*MockIRepository)(nil).GetCompanyByExternalID), ctx, companySFID) +} + +// GetCompanyByName mocks base method. +func (m *MockIRepository) GetCompanyByName(ctx context.Context, companyName string) (*models.Company, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanyByName", ctx, companyName) + ret0, _ := ret[0].(*models.Company) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanyByName indicates an expected call of GetCompanyByName. +func (mr *MockIRepositoryMockRecorder) GetCompanyByName(ctx, companyName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanyByName", reflect.TypeOf((*MockIRepository)(nil).GetCompanyByName), ctx, companyName) +} + +// GetCompanyBySigningEntityName mocks base method. +func (m *MockIRepository) GetCompanyBySigningEntityName(ctx context.Context, signingEntityName string) (*models.Company, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanyBySigningEntityName", ctx, signingEntityName) + ret0, _ := ret[0].(*models.Company) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanyBySigningEntityName indicates an expected call of GetCompanyBySigningEntityName. +func (mr *MockIRepositoryMockRecorder) GetCompanyBySigningEntityName(ctx, signingEntityName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanyBySigningEntityName", reflect.TypeOf((*MockIRepository)(nil).GetCompanyBySigningEntityName), ctx, signingEntityName) +} + +// GetCompanyInviteRequest mocks base method. +func (m *MockIRepository) GetCompanyInviteRequest(ctx context.Context, companyInviteID string) (*company.Invite, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanyInviteRequest", ctx, companyInviteID) + ret0, _ := ret[0].(*company.Invite) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanyInviteRequest indicates an expected call of GetCompanyInviteRequest. +func (mr *MockIRepositoryMockRecorder) GetCompanyInviteRequest(ctx, companyInviteID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanyInviteRequest", reflect.TypeOf((*MockIRepository)(nil).GetCompanyInviteRequest), ctx, companyInviteID) +} + +// GetCompanyInviteRequests mocks base method. +func (m *MockIRepository) GetCompanyInviteRequests(ctx context.Context, companyID string, status *string) ([]company.Invite, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanyInviteRequests", ctx, companyID, status) + ret0, _ := ret[0].([]company.Invite) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanyInviteRequests indicates an expected call of GetCompanyInviteRequests. +func (mr *MockIRepositoryMockRecorder) GetCompanyInviteRequests(ctx, companyID, status interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanyInviteRequests", reflect.TypeOf((*MockIRepository)(nil).GetCompanyInviteRequests), ctx, companyID, status) +} + +// GetCompanyUserInviteRequests mocks base method. +func (m *MockIRepository) GetCompanyUserInviteRequests(ctx context.Context, companyID, userID string) (*company.Invite, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanyUserInviteRequests", ctx, companyID, userID) + ret0, _ := ret[0].(*company.Invite) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanyUserInviteRequests indicates an expected call of GetCompanyUserInviteRequests. +func (mr *MockIRepositoryMockRecorder) GetCompanyUserInviteRequests(ctx, companyID, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanyUserInviteRequests", reflect.TypeOf((*MockIRepository)(nil).GetCompanyUserInviteRequests), ctx, companyID, userID) +} + +// GetUserInviteRequests mocks base method. +func (m *MockIRepository) GetUserInviteRequests(ctx context.Context, userID string) ([]company.Invite, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserInviteRequests", ctx, userID) + ret0, _ := ret[0].([]company.Invite) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserInviteRequests indicates an expected call of GetUserInviteRequests. +func (mr *MockIRepositoryMockRecorder) GetUserInviteRequests(ctx, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserInviteRequests", reflect.TypeOf((*MockIRepository)(nil).GetUserInviteRequests), ctx, userID) +} + +// IsCCLAEnabledForCompany mocks base method. +func (m *MockIRepository) IsCCLAEnabledForCompany(ctx context.Context, companyID string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsCCLAEnabledForCompany", ctx, companyID) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsCCLAEnabledForCompany indicates an expected call of IsCCLAEnabledForCompany. +func (mr *MockIRepositoryMockRecorder) IsCCLAEnabledForCompany(ctx, companyID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsCCLAEnabledForCompany", reflect.TypeOf((*MockIRepository)(nil).IsCCLAEnabledForCompany), ctx, companyID) +} + +// RejectCompanyAccessRequest mocks base method. +func (m *MockIRepository) RejectCompanyAccessRequest(ctx context.Context, companyInviteID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RejectCompanyAccessRequest", ctx, companyInviteID) + ret0, _ := ret[0].(error) + return ret0 +} + +// RejectCompanyAccessRequest indicates an expected call of RejectCompanyAccessRequest. +func (mr *MockIRepositoryMockRecorder) RejectCompanyAccessRequest(ctx, companyInviteID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RejectCompanyAccessRequest", reflect.TypeOf((*MockIRepository)(nil).RejectCompanyAccessRequest), ctx, companyInviteID) +} + +// SearchCompanyByName mocks base method. +func (m *MockIRepository) SearchCompanyByName(ctx context.Context, companyName, nextKey string) (*models.Companies, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchCompanyByName", ctx, companyName, nextKey) + ret0, _ := ret[0].(*models.Companies) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchCompanyByName indicates an expected call of SearchCompanyByName. +func (mr *MockIRepositoryMockRecorder) SearchCompanyByName(ctx, companyName, nextKey interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchCompanyByName", reflect.TypeOf((*MockIRepository)(nil).SearchCompanyByName), ctx, companyName, nextKey) +} + +// UpdateCompanyAccessList mocks base method. +func (m *MockIRepository) UpdateCompanyAccessList(ctx context.Context, companyID string, companyACL []string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateCompanyAccessList", ctx, companyID, companyACL) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateCompanyAccessList indicates an expected call of UpdateCompanyAccessList. +func (mr *MockIRepositoryMockRecorder) UpdateCompanyAccessList(ctx, companyID, companyACL interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCompanyAccessList", reflect.TypeOf((*MockIRepository)(nil).UpdateCompanyAccessList), ctx, companyID, companyACL) +} diff --git a/cla-backend-go/company/mocks/mock_service.go b/cla-backend-go/company/mocks/mock_service.go new file mode 100644 index 000000000..7e483727e --- /dev/null +++ b/cla-backend-go/company/mocks/mock_service.go @@ -0,0 +1,346 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +// Code generated by MockGen. DO NOT EDIT. +// Source: company/service.go + +// Package mock_company is a generated GoMock package. +package mock_company + +import ( + context "context" + reflect "reflect" + + company "github.com/communitybridge/easycla/cla-backend-go/company" + models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + gomock "github.com/golang/mock/gomock" +) + +// MockIService is a mock of IService interface. +type MockIService struct { + ctrl *gomock.Controller + recorder *MockIServiceMockRecorder +} + +// MockIServiceMockRecorder is the mock recorder for MockIService. +type MockIServiceMockRecorder struct { + mock *MockIService +} + +// NewMockIService creates a new mock instance. +func NewMockIService(ctrl *gomock.Controller) *MockIService { + mock := &MockIService{ctrl: ctrl} + mock.recorder = &MockIServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIService) EXPECT() *MockIServiceMockRecorder { + return m.recorder +} + +// AddPendingCompanyInviteRequest mocks base method. +func (m *MockIService) AddPendingCompanyInviteRequest(ctx context.Context, companyID, userID string) (*company.InviteModel, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddPendingCompanyInviteRequest", ctx, companyID, userID) + ret0, _ := ret[0].(*company.InviteModel) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddPendingCompanyInviteRequest indicates an expected call of AddPendingCompanyInviteRequest. +func (mr *MockIServiceMockRecorder) AddPendingCompanyInviteRequest(ctx, companyID, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPendingCompanyInviteRequest", reflect.TypeOf((*MockIService)(nil).AddPendingCompanyInviteRequest), ctx, companyID, userID) +} + +// AddUserToCompanyAccessList mocks base method. +func (m *MockIService) AddUserToCompanyAccessList(ctx context.Context, companyID, lfid string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddUserToCompanyAccessList", ctx, companyID, lfid) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddUserToCompanyAccessList indicates an expected call of AddUserToCompanyAccessList. +func (mr *MockIServiceMockRecorder) AddUserToCompanyAccessList(ctx, companyID, lfid interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUserToCompanyAccessList", reflect.TypeOf((*MockIService)(nil).AddUserToCompanyAccessList), ctx, companyID, lfid) +} + +// ApproveCompanyAccessRequest mocks base method. +func (m *MockIService) ApproveCompanyAccessRequest(ctx context.Context, companyInviteID string) (*company.InviteModel, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ApproveCompanyAccessRequest", ctx, companyInviteID) + ret0, _ := ret[0].(*company.InviteModel) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ApproveCompanyAccessRequest indicates an expected call of ApproveCompanyAccessRequest. +func (mr *MockIServiceMockRecorder) ApproveCompanyAccessRequest(ctx, companyInviteID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApproveCompanyAccessRequest", reflect.TypeOf((*MockIService)(nil).ApproveCompanyAccessRequest), ctx, companyInviteID) +} + +// CreateOrgFromExternalID mocks base method. +func (m *MockIService) CreateOrgFromExternalID(ctx context.Context, signingEntityName, companySFID string) (*models.Company, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateOrgFromExternalID", ctx, signingEntityName, companySFID) + ret0, _ := ret[0].(*models.Company) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateOrgFromExternalID indicates an expected call of CreateOrgFromExternalID. +func (mr *MockIServiceMockRecorder) CreateOrgFromExternalID(ctx, signingEntityName, companySFID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrgFromExternalID", reflect.TypeOf((*MockIService)(nil).CreateOrgFromExternalID), ctx, signingEntityName, companySFID) +} + +// GetCompanies mocks base method. +func (m *MockIService) GetCompanies(ctx context.Context) (*models.Companies, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanies", ctx) + ret0, _ := ret[0].(*models.Companies) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanies indicates an expected call of GetCompanies. +func (mr *MockIServiceMockRecorder) GetCompanies(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanies", reflect.TypeOf((*MockIService)(nil).GetCompanies), ctx) +} + +// GetCompaniesByExternalID mocks base method. +func (m *MockIService) GetCompaniesByExternalID(ctx context.Context, companySFID string, includeChildCompanies bool) ([]*models.Company, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompaniesByExternalID", ctx, companySFID, includeChildCompanies) + ret0, _ := ret[0].([]*models.Company) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompaniesByExternalID indicates an expected call of GetCompaniesByExternalID. +func (mr *MockIServiceMockRecorder) GetCompaniesByExternalID(ctx, companySFID, includeChildCompanies interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompaniesByExternalID", reflect.TypeOf((*MockIService)(nil).GetCompaniesByExternalID), ctx, companySFID, includeChildCompanies) +} + +// GetCompaniesByUserManager mocks base method. +func (m *MockIService) GetCompaniesByUserManager(ctx context.Context, userID string) (*models.Companies, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompaniesByUserManager", ctx, userID) + ret0, _ := ret[0].(*models.Companies) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompaniesByUserManager indicates an expected call of GetCompaniesByUserManager. +func (mr *MockIServiceMockRecorder) GetCompaniesByUserManager(ctx, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompaniesByUserManager", reflect.TypeOf((*MockIService)(nil).GetCompaniesByUserManager), ctx, userID) +} + +// GetCompaniesByUserManagerWithInvites mocks base method. +func (m *MockIService) GetCompaniesByUserManagerWithInvites(ctx context.Context, userID string) (*models.CompaniesWithInvites, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompaniesByUserManagerWithInvites", ctx, userID) + ret0, _ := ret[0].(*models.CompaniesWithInvites) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompaniesByUserManagerWithInvites indicates an expected call of GetCompaniesByUserManagerWithInvites. +func (mr *MockIServiceMockRecorder) GetCompaniesByUserManagerWithInvites(ctx, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompaniesByUserManagerWithInvites", reflect.TypeOf((*MockIService)(nil).GetCompaniesByUserManagerWithInvites), ctx, userID) +} + +// GetCompany mocks base method. +func (m *MockIService) GetCompany(ctx context.Context, companyID string) (*models.Company, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompany", ctx, companyID) + ret0, _ := ret[0].(*models.Company) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompany indicates an expected call of GetCompany. +func (mr *MockIServiceMockRecorder) GetCompany(ctx, companyID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompany", reflect.TypeOf((*MockIService)(nil).GetCompany), ctx, companyID) +} + +// GetCompanyByExternalID mocks base method. +func (m *MockIService) GetCompanyByExternalID(ctx context.Context, companySFID string) (*models.Company, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanyByExternalID", ctx, companySFID) + ret0, _ := ret[0].(*models.Company) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanyByExternalID indicates an expected call of GetCompanyByExternalID. +func (mr *MockIServiceMockRecorder) GetCompanyByExternalID(ctx, companySFID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanyByExternalID", reflect.TypeOf((*MockIService)(nil).GetCompanyByExternalID), ctx, companySFID) +} + +// GetCompanyBySigningEntityName mocks base method. +func (m *MockIService) GetCompanyBySigningEntityName(ctx context.Context, signingEntityName, companySFID string) (*models.Company, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanyBySigningEntityName", ctx, signingEntityName, companySFID) + ret0, _ := ret[0].(*models.Company) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanyBySigningEntityName indicates an expected call of GetCompanyBySigningEntityName. +func (mr *MockIServiceMockRecorder) GetCompanyBySigningEntityName(ctx, signingEntityName, companySFID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanyBySigningEntityName", reflect.TypeOf((*MockIService)(nil).GetCompanyBySigningEntityName), ctx, signingEntityName, companySFID) +} + +// GetCompanyInviteRequests mocks base method. +func (m *MockIService) GetCompanyInviteRequests(ctx context.Context, companyID string, status *string) ([]models.CompanyInviteUser, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanyInviteRequests", ctx, companyID, status) + ret0, _ := ret[0].([]models.CompanyInviteUser) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanyInviteRequests indicates an expected call of GetCompanyInviteRequests. +func (mr *MockIServiceMockRecorder) GetCompanyInviteRequests(ctx, companyID, status interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanyInviteRequests", reflect.TypeOf((*MockIService)(nil).GetCompanyInviteRequests), ctx, companyID, status) +} + +// GetCompanyUserInviteRequests mocks base method. +func (m *MockIService) GetCompanyUserInviteRequests(ctx context.Context, companyID, userID string) (*models.CompanyInviteUser, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanyUserInviteRequests", ctx, companyID, userID) + ret0, _ := ret[0].(*models.CompanyInviteUser) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanyUserInviteRequests indicates an expected call of GetCompanyUserInviteRequests. +func (mr *MockIServiceMockRecorder) GetCompanyUserInviteRequests(ctx, companyID, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanyUserInviteRequests", reflect.TypeOf((*MockIService)(nil).GetCompanyUserInviteRequests), ctx, companyID, userID) +} + +// IsCCLAEnabledForCompany mocks base method. +func (m *MockIService) IsCCLAEnabledForCompany(ctx context.Context, companySFID string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsCCLAEnabledForCompany", ctx, companySFID) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsCCLAEnabledForCompany indicates an expected call of IsCCLAEnabledForCompany. +func (mr *MockIServiceMockRecorder) IsCCLAEnabledForCompany(ctx, companySFID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsCCLAEnabledForCompany", reflect.TypeOf((*MockIService)(nil).IsCCLAEnabledForCompany), ctx, companySFID) +} + +// RejectCompanyAccessRequest mocks base method. +func (m *MockIService) RejectCompanyAccessRequest(ctx context.Context, companyInviteID string) (*company.InviteModel, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RejectCompanyAccessRequest", ctx, companyInviteID) + ret0, _ := ret[0].(*company.InviteModel) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RejectCompanyAccessRequest indicates an expected call of RejectCompanyAccessRequest. +func (mr *MockIServiceMockRecorder) RejectCompanyAccessRequest(ctx, companyInviteID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RejectCompanyAccessRequest", reflect.TypeOf((*MockIService)(nil).RejectCompanyAccessRequest), ctx, companyInviteID) +} + +// SearchCompanyByName mocks base method. +func (m *MockIService) SearchCompanyByName(ctx context.Context, companyName, nextKey string) (*models.Companies, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchCompanyByName", ctx, companyName, nextKey) + ret0, _ := ret[0].(*models.Companies) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchCompanyByName indicates an expected call of SearchCompanyByName. +func (mr *MockIServiceMockRecorder) SearchCompanyByName(ctx, companyName, nextKey interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchCompanyByName", reflect.TypeOf((*MockIService)(nil).SearchCompanyByName), ctx, companyName, nextKey) +} + +// SearchOrganizationByName mocks base method. +func (m *MockIService) SearchOrganizationByName(ctx context.Context, orgName, websiteName string, includeSigningEntityName bool, filter string) (*models.OrgList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchOrganizationByName", ctx, orgName, websiteName, includeSigningEntityName, filter) + ret0, _ := ret[0].(*models.OrgList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchOrganizationByName indicates an expected call of SearchOrganizationByName. +func (mr *MockIServiceMockRecorder) SearchOrganizationByName(ctx, orgName, websiteName, includeSigningEntityName, filter interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchOrganizationByName", reflect.TypeOf((*MockIService)(nil).SearchOrganizationByName), ctx, orgName, websiteName, includeSigningEntityName, filter) +} + +// getPreferredNameAndEmail mocks base method. +func (m *MockIService) getPreferredNameAndEmail(ctx context.Context, lfid string) (string, string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "getPreferredNameAndEmail", ctx, lfid) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// getPreferredNameAndEmail indicates an expected call of getPreferredNameAndEmail. +func (mr *MockIServiceMockRecorder) getPreferredNameAndEmail(ctx, lfid interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "getPreferredNameAndEmail", reflect.TypeOf((*MockIService)(nil).getPreferredNameAndEmail), ctx, lfid) +} + +// sendRequestAccessEmail mocks base method. +func (m *MockIService) sendRequestAccessEmail(ctx context.Context, companyModel *models.Company, requesterName, requesterEmail, recipientName, recipientAddress string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "sendRequestAccessEmail", ctx, companyModel, requesterName, requesterEmail, recipientName, recipientAddress) +} + +// sendRequestAccessEmail indicates an expected call of sendRequestAccessEmail. +func (mr *MockIServiceMockRecorder) sendRequestAccessEmail(ctx, companyModel, requesterName, requesterEmail, recipientName, recipientAddress interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "sendRequestAccessEmail", reflect.TypeOf((*MockIService)(nil).sendRequestAccessEmail), ctx, companyModel, requesterName, requesterEmail, recipientName, recipientAddress) +} + +// sendRequestApprovedEmailToRecipient mocks base method. +func (m *MockIService) sendRequestApprovedEmailToRecipient(ctx context.Context, companyModel *models.Company, recipientName, recipientAddress string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "sendRequestApprovedEmailToRecipient", ctx, companyModel, recipientName, recipientAddress) +} + +// sendRequestApprovedEmailToRecipient indicates an expected call of sendRequestApprovedEmailToRecipient. +func (mr *MockIServiceMockRecorder) sendRequestApprovedEmailToRecipient(ctx, companyModel, recipientName, recipientAddress interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "sendRequestApprovedEmailToRecipient", reflect.TypeOf((*MockIService)(nil).sendRequestApprovedEmailToRecipient), ctx, companyModel, recipientName, recipientAddress) +} + +// sendRequestRejectedEmailToRecipient mocks base method. +func (m *MockIService) sendRequestRejectedEmailToRecipient(ctx context.Context, companyModel *models.Company, recipientName, recipientAddress string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "sendRequestRejectedEmailToRecipient", ctx, companyModel, recipientName, recipientAddress) +} + +// sendRequestRejectedEmailToRecipient indicates an expected call of sendRequestRejectedEmailToRecipient. +func (mr *MockIServiceMockRecorder) sendRequestRejectedEmailToRecipient(ctx, companyModel, recipientName, recipientAddress interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "sendRequestRejectedEmailToRecipient", reflect.TypeOf((*MockIService)(nil).sendRequestRejectedEmailToRecipient), ctx, companyModel, recipientName, recipientAddress) +} diff --git a/cla-backend-go/company/models.go b/cla-backend-go/company/models.go index 9518086d9..9b4fea85a 100644 --- a/cla-backend-go/company/models.go +++ b/cla-backend-go/company/models.go @@ -6,7 +6,7 @@ package company import ( "context" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/go-openapi/strfmt" @@ -90,11 +90,13 @@ func (dbCompanyModel *DBModel) toModel() (*models.Company, error) { } // dbModelsToResponseModels is a helper routine to convert the (internal) database model to a (public) swagger model -func dbModelsToResponseModels(ctx context.Context, dbModels []DBModel) ([]*models.Company, error) { +func dbModelsToResponseModels(ctx context.Context, dbModels []DBModel, includeChildCompanies bool) ([]*models.Company, error) { f := logrus.Fields{ - "functionName": "company.models.dbModelsToResponseModels", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "functionName": "company.models.dbModelsToResponseModels", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "includeChildCompanies": includeChildCompanies, } + var companyModels []*models.Company var err error for _, dbModel := range dbModels { @@ -103,8 +105,16 @@ func dbModelsToResponseModels(ctx context.Context, dbModels []DBModel) ([]*model log.WithFields(f).WithError(conversionErr).Warn("unable to convert db model to company model") err = conversionErr } else { - log.WithFields(f).Debugf("Converted %+v to %+v", dbModel, respModel) - companyModels = append(companyModels, respModel) + // log.WithFields(f).Debugf("Converted %+v to %+v", dbModel, respModel) + if includeChildCompanies { + companyModels = append(companyModels, respModel) + } else { + // only include if company is not a signing entity name with different name + if respModel.SigningEntityName == "" || respModel.CompanyName == respModel.SigningEntityName { + companyModels = append(companyModels, respModel) + break // no need to continue + } + } } } diff --git a/cla-backend-go/company/repository.go b/cla-backend-go/company/repository.go index 2708c608d..91e521301 100644 --- a/cla-backend-go/company/repository.go +++ b/cla-backend-go/company/repository.go @@ -5,7 +5,6 @@ package company import ( "context" - "errors" "fmt" "strings" @@ -17,7 +16,7 @@ import ( "github.com/go-openapi/strfmt" "github.com/aws/aws-sdk-go/service/dynamodb/expression" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" log "github.com/communitybridge/easycla/cla-backend-go/logging" @@ -28,9 +27,8 @@ import ( "github.com/gofrs/uuid" ) -// errors -var ( - ErrCompanyDoesNotExist = errors.New("company does not exist") +const ( + SignatureReferenceIndex = "reference-signature-index" ) // IRepository interface methods @@ -39,7 +37,7 @@ type IRepository interface { //nolint GetCompanies(ctx context.Context) (*models.Companies, error) GetCompany(ctx context.Context, companyID string) (*models.Company, error) GetCompanyByExternalID(ctx context.Context, companySFID string) (*models.Company, error) - GetCompaniesByExternalID(ctx context.Context, companySFID string) ([]*models.Company, error) + GetCompaniesByExternalID(ctx context.Context, companySFID string, includeChildCompanies bool) ([]*models.Company, error) GetCompanyBySigningEntityName(ctx context.Context, signingEntityName string) (*models.Company, error) GetCompanyByName(ctx context.Context, companyName string) (*models.Company, error) SearchCompanyByName(ctx context.Context, companyName string, nextKey string) (*models.Companies, error) @@ -47,7 +45,6 @@ type IRepository interface { //nolint DeleteCompanyBySFID(ctx context.Context, companySFID string) error GetCompaniesByUserManager(ctx context.Context, userID string, userModel user.User) (*models.Companies, error) GetCompaniesByUserManagerWithInvites(ctx context.Context, userID string, userModel user.User) (*models.CompaniesWithInvites, error) - AddPendingCompanyInviteRequest(ctx context.Context, companyID string, userModel user.User) (*Invite, error) GetCompanyInviteRequest(ctx context.Context, companyInviteID string) (*Invite, error) GetCompanyInviteRequests(ctx context.Context, companyID string, status *string) ([]Invite, error) @@ -55,15 +52,15 @@ type IRepository interface { //nolint GetUserInviteRequests(ctx context.Context, userID string) ([]Invite, error) ApproveCompanyAccessRequest(ctx context.Context, companyInviteID string) error RejectCompanyAccessRequest(ctx context.Context, companyInviteID string) error - updateInviteRequestStatus(ctx context.Context, companyInviteID, status string) error - UpdateCompanyAccessList(ctx context.Context, companyID string, companyACL []string) error + IsCCLAEnabledForCompany(ctx context.Context, companyID string) (bool, error) } type repository struct { stage string dynamoDBClient *dynamodb.DynamoDB companyTableName string + signatureTableName string companyInvitesTableName string } @@ -73,6 +70,7 @@ func NewRepository(awsSession *session.Session, stage string) IRepository { stage: stage, dynamoDBClient: dynamodb.New(awsSession), companyTableName: fmt.Sprintf("cla-%s-companies", stage), + signatureTableName: fmt.Sprintf("cla-%s-signatures", stage), companyInvitesTableName: fmt.Sprintf("cla-%s-company-invites", stage), } } @@ -163,48 +161,34 @@ func (repo repository) GetCompanyByExternalID(ctx context.Context, companySFID s "companySFID": companySFID, } - // Historically, we would only have zero or one companySF record in the DB. In v2 we introduced the Signing Entity - // Name concept where we would create a new EasyCLA record in the DB for each signing entity name - this allowed CLA - // Managers/Designee to have separate signature records pointing to separate company records. As a result, these - // new companies would also be attached to the same SF parent company results in this API response returning zero or - // more records. To make this backwards compatible for v1, we will still honor this API call and return the company - // record where the entity name is either missing or the same as the company name. - companyRecords, err := repo.GetCompaniesByExternalID(ctx, companySFID) + const includeChildCompanies = false // Include child/other signing entity name records? + companyRecords, err := repo.GetCompaniesByExternalID(ctx, companySFID, includeChildCompanies) if err != nil { log.WithFields(f).WithError(err).Warn("unable to unmarshall response from the database") return nil, err } + log.WithFields(f).Debugf("loaded %d records", len(companyRecords)) + if len(companyRecords) == 0 { log.WithFields(f).Debug("no records found") - return nil, ErrCompanyDoesNotExist - } - log.WithFields(f).Debugf("loaded %d records", len(companyRecords)) - // For debug when problems occur - f["companyName"] = companyRecords[0].CompanyName - var signingEntityNames []string - - // To support backward compatibility, search for the case where the signing entity name is empty or where the - // signing entity name matches the company name - for _, companyModel := range companyRecords { - // Save in case we can't find it - we'll show on the output - signingEntityNames = append(signingEntityNames, companyModel.SigningEntityName) - // If this is our record... - if companyModel.SigningEntityName == "" || companyModel.SigningEntityName == companyModel.CompanyName { - return companyModel, nil + return nil, &utils.CompanyNotFound{ + Message: "no company records found for SFID", + CompanyID: companySFID, } } - f["signingEntityNames"] = strings.Join(signingEntityNames, ";") - log.WithFields(f).Warning("unable to match company name with existing signing entity names") - return nil, ErrCompanyDoesNotExist + + return companyRecords[0], nil } // GetCompaniesByExternalID returns a list of companies based on the company external ID. A company will have more than one if/when the SF record has multiple entity names - for which we create separate EasyCLA company records -func (repo repository) GetCompaniesByExternalID(ctx context.Context, companySFID string) ([]*models.Company, error) { +func (repo repository) GetCompaniesByExternalID(ctx context.Context, companySFID string, includeChildCompanies bool) ([]*models.Company, error) { f := logrus.Fields{ - "functionName": "company.repository.GetCompaniesByExternalID", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "companySFID": companySFID, + "functionName": "company.repository.GetCompaniesByExternalID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companySFID": companySFID, + "includeChildCompanies": includeChildCompanies, } + condition := expression.Key("company_external_id").Equal(expression.Value(companySFID)) builder := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildCompanyProjection()) // Use the nice builder to create the expression @@ -232,9 +216,13 @@ func (repo repository) GetCompaniesByExternalID(ctx context.Context, companySFID } if len(results.Items) == 0 { - log.WithFields(f).Debug("no records found") - return nil, ErrCompanyDoesNotExist + log.WithFields(f).Debug("no company records found") + return nil, &utils.CompanyNotFound{ + Message: "no company records found with matching external SFID", + CompanySFID: companySFID, + } } + var dbCompanyModels []DBModel err = dynamodbattribute.UnmarshalListOfMaps(results.Items, &dbCompanyModels) if err != nil { @@ -243,7 +231,7 @@ func (repo repository) GetCompaniesByExternalID(ctx context.Context, companySFID } log.WithFields(f).Debug("converting database records to a response model...") - return dbModelsToResponseModels(ctx, dbCompanyModels) + return dbModelsToResponseModels(ctx, dbCompanyModels, includeChildCompanies) } // GetCompanyBySigningEntityName search the company by signing entity name @@ -279,8 +267,13 @@ func (repo repository) GetCompanyBySigningEntityName(ctx context.Context, signin } if len(results.Items) == 0 { - return nil, ErrCompanyDoesNotExist + return nil, &utils.CompanyNotFound{ + Message: "no company with signing entity name found", + CompanySigningEntityName: signingEntityName, + Err: nil, + } } + dbCompanyModel := DBModel{} err = dynamodbattribute.UnmarshalMap(results.Items[0], &dbCompanyModel) if err != nil { @@ -366,7 +359,10 @@ func (repo repository) GetCompany(ctx context.Context, companyID string) (*model } if len(companyTableData.Item) == 0 { - return nil, ErrCompanyDoesNotExist + return nil, &utils.CompanyNotFound{ + Message: "no company matching company record", + CompanyID: companyID, + } } dbCompanyModel := DBModel{} @@ -642,6 +638,50 @@ func (repo repository) GetCompaniesByUserManager(ctx context.Context, userID str }, nil } +// IsCCLAEnabled returns true if company is enabled for CCLA +func (repo repository) IsCCLAEnabledForCompany(ctx context.Context, companyID string) (bool, error) { + f := logrus.Fields{ + "functionName": "v1.signature.repository.IsCCLAEnabled", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companyID": companyID, + } + + // Build the query + condition := expression.Key("signature_reference_id").Equal(expression.Value(companyID)) + + filter := expression.Name("signature_signed").Equal(expression.Value(true)).And(expression.Name("signature_approved").Equal(expression.Value(true))).And(expression.Name("signature_type").Equal(expression.Value("ccla"))) + + expr, err := expression.NewBuilder().WithKeyCondition(condition).WithFilter(filter).Build() + + if err != nil { + log.WithFields(f).Warnf("error building expression for company: %s, error: %v", companyID, err) + return false, err + } + + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + FilterExpression: expr.Filter(), + TableName: aws.String(repo.signatureTableName), + IndexName: aws.String(SignatureReferenceIndex), + } + + results, queryErr := repo.dynamoDBClient.QueryWithContext(ctx, queryInput) + if queryErr != nil { + log.WithFields(f).Warnf("error querying signatures for company: %s, error: %v", companyID, queryErr) + return false, queryErr + } + + if *results.Count > 0 { + log.WithFields(f).Debugf("company: %s is enabled for CCLA", companyID) + return true, nil + } + + return false, nil + +} + // GetCompanyUserManagerWithInvites the get a list of companies including status when provided the company id and user manager func (repo repository) GetCompaniesByUserManagerWithInvites(ctx context.Context, userID string, userModel user.User) (*models.CompaniesWithInvites, error) { f := logrus.Fields{ @@ -1225,14 +1265,31 @@ func (repo repository) UpdateCompanyAccessList(ctx context.Context, companyID st // CreateCompany creates a new company record func (repo repository) CreateCompany(ctx context.Context, in *models.Company) (*models.Company, error) { f := logrus.Fields{ - "functionName": "company.repository.CreateCompany", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "functionName": "company.repository.CreateCompany", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companyName": in.CompanyName, + "signingEntityName": in.SigningEntityName, + "companySFID": in.CompanyExternalID, + } + + // Don't create duplicates - check to see if any exist + existingModel, queryErr := repo.GetCompanyByName(ctx, in.CompanyName) + if queryErr != nil { + log.WithFields(f).WithError(queryErr).Warn("problem querying for existing company record by name") + return nil, queryErr } + // Already exists - don't re-create + if existingModel != nil { + return existingModel, nil + } + companyID, err := uuid.NewV4() if err != nil { log.WithFields(f).Warnf("Unable to generate a UUID for a pending invite, error: %v", err) return nil, err } + f["companyID"] = companyID + _, now := utils.CurrentTime() comp := &DBModel{ CompanyID: companyID.String(), @@ -1268,6 +1325,7 @@ func (repo repository) CreateCompany(ctx context.Context, in *models.Company) (* TableName: aws.String(repo.companyTableName), }) if err != nil { + log.WithFields(f).WithError(err).Warn("problem creating new company") return nil, err } diff --git a/cla-backend-go/company/service.go b/cla-backend-go/company/service.go index dabfaa1f9..87dfb59c6 100644 --- a/cla-backend-go/company/service.go +++ b/cla-backend-go/company/service.go @@ -6,6 +6,11 @@ package company import ( "context" "fmt" + "sort" + "strings" + "sync" + + "github.com/go-openapi/strfmt" "github.com/sirupsen/logrus" @@ -13,11 +18,12 @@ import ( "github.com/communitybridge/easycla/cla-backend-go/utils" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/user" organization_service "github.com/communitybridge/easycla/cla-backend-go/v2/organization-service" "github.com/communitybridge/easycla/cla-backend-go/v2/organization-service/client/organizations" + orgServiceModels "github.com/communitybridge/easycla/cla-backend-go/v2/organization-service/models" ) type service struct { @@ -39,7 +45,7 @@ type IService interface { // nolint GetCompanies(ctx context.Context) (*models.Companies, error) GetCompany(ctx context.Context, companyID string) (*models.Company, error) GetCompanyByExternalID(ctx context.Context, companySFID string) (*models.Company, error) - GetCompaniesByExternalID(ctx context.Context, companySFID string) ([]*models.Company, error) + GetCompaniesByExternalID(ctx context.Context, companySFID string, includeChildCompanies bool) ([]*models.Company, error) GetCompanyBySigningEntityName(ctx context.Context, signingEntityName, companySFID string) (*models.Company, error) SearchCompanyByName(ctx context.Context, companyName string, nextKey string) (*models.Companies, error) GetCompaniesByUserManager(ctx context.Context, userID string) (*models.Companies, error) @@ -51,14 +57,15 @@ type IService interface { // nolint AddPendingCompanyInviteRequest(ctx context.Context, companyID string, userID string) (*InviteModel, error) ApproveCompanyAccessRequest(ctx context.Context, companyInviteID string) (*InviteModel, error) RejectCompanyAccessRequest(ctx context.Context, companyInviteID string) (*InviteModel, error) + IsCCLAEnabledForCompany(ctx context.Context, companySFID string) (bool, error) // calls org service - SearchOrganizationByName(ctx context.Context, orgName string, websiteName string, filter string) (*models.OrgList, error) + SearchOrganizationByName(ctx context.Context, orgName string, websiteName string, includeSigningEntityName bool, filter string) (*models.OrgList, error) - sendRequestAccessEmail(ctx context.Context, companyModel *models.Company, requesterName, requesterEmail, recipientName, recipientAddress string) - sendRequestApprovedEmailToRecipient(ctx context.Context, companyModel *models.Company, recipientName, recipientAddress string) - sendRequestRejectedEmailToRecipient(ctx context.Context, companyModel *models.Company, recipientName, recipientAddress string) - getPreferredNameAndEmail(ctx context.Context, lfid string) (string, string, error) + // sendRequestAccessEmail(ctx context.Context, companyModel *models.Company, requesterName, requesterEmail, recipientName, recipientAddress string) + // sendRequestApprovedEmailToRecipient(ctx context.Context, companyModel *models.Company, recipientName, recipientAddress string) + // sendRequestRejectedEmailToRecipient(ctx context.Context, companyModel *models.Company, recipientName, recipientAddress string) + // getPreferredNameAndEmail(ctx context.Context, lfid string) (string, string, error) } // NewService creates a new company service object @@ -430,6 +437,11 @@ func (s service) AddUserToCompanyAccessList(ctx context.Context, companyID, lfid return nil } +// IsCCLAEnabledForCompany determines if the specified company has CCLA enabled +func (s service) IsCCLAEnabledForCompany(ctx context.Context, companyID string) (bool, error) { + return s.repo.IsCCLAEnabledForCompany(ctx, companyID) +} + // sendRequestAccessEmail sends the request access email func (s service) sendRequestAccessEmail(ctx context.Context, companyModel *models.Company, requesterName, requesterEmail, recipientName, recipientAddress string) { f := logrus.Fields{ @@ -615,10 +627,10 @@ func (s service) getPreferredNameAndEmail(ctx context.Context, lfid string) (str userEmail := userModel.LfEmail if userEmail == "" && userModel.Emails != nil && len(userModel.Emails) > 0 { - userEmail = userModel.Emails[0] + userEmail = strfmt.Email(userModel.Emails[0]) } - return userName, userEmail, nil + return userName, userEmail.String(), nil } func (s service) GetCompanyByExternalID(ctx context.Context, companySFID string) (*models.Company, error) { @@ -634,7 +646,7 @@ func (s service) GetCompanyByExternalID(ctx context.Context, companySFID string) return comp, nil } - if err == ErrCompanyDoesNotExist { + if _, ok := err.(*utils.CompanyNotFound); ok { comp, err = s.CreateOrgFromExternalID(ctx, "", companySFID) if err != nil { return comp, err @@ -644,15 +656,16 @@ func (s service) GetCompanyByExternalID(ctx context.Context, companySFID string) return nil, err } -func (s service) GetCompaniesByExternalID(ctx context.Context, companySFID string) ([]*models.Company, error) { +func (s service) GetCompaniesByExternalID(ctx context.Context, companySFID string, includeChildCompanies bool) ([]*models.Company, error) { f := logrus.Fields{ - "functionName": "company.service.GetCompaniesByExternalID", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "companySFID": companySFID, + "functionName": "company.service.GetCompaniesByExternalID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companySFID": companySFID, + "includeChildCompanies": includeChildCompanies, } log.WithFields(f).Debug("Searching companies by external ID...") - comp, err := s.repo.GetCompaniesByExternalID(ctx, companySFID) + comp, err := s.repo.GetCompaniesByExternalID(ctx, companySFID, includeChildCompanies) if err != nil { log.WithFields(f).WithError(err).Warn("unable to locate matching records by companySFID") return nil, err @@ -670,31 +683,22 @@ func (s service) GetCompanyBySigningEntityName(ctx context.Context, signingEntit } log.WithFields(f).Debug("Searching company by signing entity name...") comp, err := s.repo.GetCompanyBySigningEntityName(ctx, signingEntityName) - if err == nil { + if err != nil { log.WithFields(f).WithError(err).Warn("problem searching organizations by signing entity name") - return comp, nil - } - - if err == ErrCompanyDoesNotExist { - log.WithFields(f).Debugf("Company with signing entity name %s does not exist", signingEntityName) - comp, err = s.CreateOrgFromExternalID(ctx, signingEntityName, companySFID) - if err != nil { - log.WithFields(f).WithError(err).Warnf("Unable to create organization from external ID: %s using signing entity name: %s", companySFID, signingEntityName) - return comp, err - } - return comp, nil + return nil, err } - return nil, err + return comp, nil } -func (s service) SearchOrganizationByName(ctx context.Context, orgName string, websiteName string, filter string) (*models.OrgList, error) { +func (s service) SearchOrganizationByName(ctx context.Context, orgName string, websiteName string, includeSigningEntityName bool, filter string) (*models.OrgList, error) { f := logrus.Fields{ - "functionName": "company.service.SearchOrganizationByName", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "orgName": orgName, - "websiteName": websiteName, - "filter": filter, + "functionName": "company.service.SearchOrganizationByName", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "orgName": orgName, + "websiteName": websiteName, + "includeSigningEntityName": includeSigningEntityName, + "filter": filter, } osc := organization_service.GetClient() @@ -705,19 +709,93 @@ func (s service) SearchOrganizationByName(ctx context.Context, orgName string, w return nil, err } - result := &models.OrgList{List: make([]*models.Org, 0, len(orgs))} + resultsChannel := make(chan *models.Org, len(orgs)) + var wg sync.WaitGroup + + wg.Add(len(orgs)) for _, org := range orgs { - var signingEntityNames []string - if len(org.SigningEntityName) > 0 { - signingEntityNames = utils.TrimSpaceFromItems(org.SigningEntityName) - } - result.List = append(result.List, &models.Org{ - OrganizationID: org.ID, - OrganizationName: org.Name, - SigningEntityNames: signingEntityNames, - OrganizationWebsite: org.Link, - }) + go func(org *orgServiceModels.Organization) { + defer wg.Done() + // get company by external ID + cclaEnabled := false + company, err := s.repo.GetCompanyByExternalID(ctx, org.ID) + if err != nil { + if _, ok := err.(*utils.CompanyNotFound); ok { + // company not found, so ccla is not enabled + log.WithFields(f).WithError(err).Warnf("company not found by name: %s", org.Name) + cclaEnabled = false + } else { + log.WithFields(f).WithError(err).Warnf("problem searching company by external ID: %s", org.ID) + return + } + } + + if company != nil { + cclaEnabled, err = s.IsCCLAEnabledForCompany(ctx, company.CompanyID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem checking if ccla is enabled for company: %s", company.CompanyID) + return + } + } + + if includeSigningEntityName { + + if len(org.SigningEntityName) > 0 { + var signingEntityNames []string + if len(org.SigningEntityName) > 0 { + signingEntityNames = utils.TrimSpaceFromItems(org.SigningEntityName) + for _, signingEntityName := range signingEntityNames { + // Auto-create the internal record, if needed + _, err = s.CreateOrgFromExternalID(ctx, signingEntityName, org.ID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("Unable to create organization from external ID: %s using signing entity name: %s", org.ID, signingEntityName) + } + } + resultsChannel <- &models.Org{ + OrganizationID: org.ID, + OrganizationName: org.Name, + SigningEntityNames: signingEntityNames, + OrganizationWebsite: org.Link, + CclaEnabled: &cclaEnabled, + } + + } + } + } else { + resultsChannel <- &models.Org{ + OrganizationID: org.ID, + OrganizationName: org.Name, + OrganizationWebsite: org.Link, + CclaEnabled: &cclaEnabled, + } + } + }(org) + } + + go func() { + wg.Wait() + close(resultsChannel) + }() + + result := &models.OrgList{ + List: make([]*models.Org, 0), } + + for orgResult := range resultsChannel { + result.List = append(result.List, orgResult) + } + + // Sort the results + sort.Slice(result.List, func(i, j int) bool { + switch strings.Compare(strings.ToLower(result.List[i].OrganizationName), strings.ToLower(result.List[j].OrganizationName)) { + case -1: + return true + case 1: + return false + } + return strings.ToLower(result.List[i].OrganizationWebsite) > strings.ToLower(result.List[j].OrganizationWebsite) + }) + return result, nil } @@ -729,8 +807,23 @@ func (s service) CreateOrgFromExternalID(ctx context.Context, signingEntityName, "companySFID": companySFID, "signingEntityName": signingEntityName, } + + var companyModel *models.Company + var lookupErr error + + // Lookup the company in our database...does it exist? + companyModel, lookupErr = s.GetCompanyBySigningEntityName(ctx, signingEntityName, companySFID) + if lookupErr != nil { + log.WithFields(f).WithError(lookupErr).Debug("problem locating internal company record by signing entity name and SFID - must not exist yet") + } + + // Already exists - no need to create in our own database + if companyModel != nil { + return companyModel, nil + } + osc := organization_service.GetClient() - log.WithFields(f).Debugf("Searching organization by company SFID...") + log.WithFields(f).Debugf("Searching organization by company SFID in the organization service...") org, err := osc.GetOrganization(ctx, companySFID) if err != nil { log.WithFields(f).WithError(err).Warn("getting organization details failed") @@ -828,7 +921,7 @@ func getCompanyAdmin(ctx context.Context, companySFID string) (*models.User, err for _, rs := range usc.RoleScopes { if rs.RoleName == "company-admin" { companyAdmin := &models.User{ - LfEmail: usc.Contact.EmailAddress, + LfEmail: strfmt.Email(usc.Contact.EmailAddress), LfUsername: usc.Contact.Username, UserExternalID: usc.Contact.ID, Username: usc.Contact.Name, diff --git a/cla-backend-go/config/config.go b/cla-backend-go/config/config.go index c562cdeed..1e89714ea 100644 --- a/cla-backend-go/config/config.go +++ b/cla-backend-go/config/config.go @@ -21,8 +21,18 @@ type Config struct { // Auth0Platform config Auth0Platform Auth0Platform `json:"auth0_platform"` - // API GW + // APIGatewayURL is the API gateway URL - old variable which is set by the old cla-auth0-gateway SSM key APIGatewayURL string `json:"api_gateway_url"` + // PlatformAPIGatewayURL is the platform API gateway URL + PlatformAPIGatewayURL string `json:"platform_api_gateway_url"` + + // EnableCLAServiceForParent is a configuration flag to indicate if we should set the enable_services=[CLA] attribute on the parent project object in the project service when a child project is associated with a CLA group. This determines the v2 project console experience/behavior." + EnableCLAServiceForParent bool `json:"enable_cla_service_for_parent"` + + // SignatureQueryDefault is a flag to indicate how a default signature query should return data - show only 'active' signatures or 'all' signatures when no other query signed/approved params are provided + SignatureQueryDefault string `json:"signature_query_default"` + // SignatureQueryDefaultValue the default value for the SignatureQueryDefault configuration value + SignatureQueryDefaultValue string `json:"signature_query_default_value"` // SFDC @@ -38,8 +48,11 @@ type Config struct { // AWS AWS AWS `json:"aws"` - // Github Application - Github Github `json:"github"` + // GitHub Application + GitHub GitHub `json:"github"` + + // Gitlab Application + Gitlab Gitlab `json:"gitlab"` // Dynamo Session Store SessionStoreTableName string `json:"sessionStoreTableName"` @@ -51,8 +64,12 @@ type Config struct { AllowedOrigins []string `json:"-"` CorporateConsoleURL string `json:"corporateConsoleURL"` + CorporateConsoleV1URL string `json:"corporateConsoleV1URL"` CorporateConsoleV2URL string `json:"corporateConsoleV2URL"` + CLAContributorv2Base string `json:"cla-contributor-v2-base"` + ClaAPIV4Base string `json:"cla_api_v4_base"` + // SNSEventTopic the topic ARN for events SNSEventTopicARN string `json:"snsEventTopicARN"` @@ -65,6 +82,12 @@ type Config struct { // CLAV1ApiURL is api url of v1. it is used in v2 sign service ClaV1ApiURL string `json:"cla_v1_api_url"` + // CLALandingPage + CLALandingPage string `json:"cla_landing_page"` + + // CLALogoURL easyCLA bot LOGO url + CLALogoURL string `json:"cla_logo_url"` + // AcsAPIKey is api key of the acs AcsAPIKey string `json:"acs_api_key"` @@ -73,6 +96,9 @@ type Config struct { // MetricsReport has the transport config to send the metrics data MetricsReport MetricsReport `json:"metrics_report"` + + // DocuSignPrivateKey is the private key for the DocuSign API + DocuSignPrivateKey string `json:"docuSignPrivateKey"` } // Auth0 model @@ -110,13 +136,26 @@ type AWS struct { Region string `json:"region"` } -// Github model -type Github struct { - ClientID string `json:"clientId"` - ClientSecret string `json:"clientSecret"` - AccessToken string `json:"accessToken"` - AppID int `json:"app_id"` - AppPrivateKey string `json:"app_private_key"` +// GitHub model +type GitHub struct { + ClientID string `json:"clientId"` + ClientSecret string `json:"clientSecret"` + AccessToken string `json:"accessToken"` + AppID int `json:"app_id"` + AppPrivateKey string `json:"app_private_key"` + TestOrganization string `json:"test_organization"` + TestOrganizationInstallationID string `json:"test_organization_installation_id"` + TestRepository string `json:"test_repository"` + TestRepositoryID string `json:"test_repository_id"` +} + +// Gitlab config data model +type Gitlab struct { + AppClientID string `json:"app_client_id"` + AppClientSecret string `json:"app_client_secret"` + AppPrivateKey string `json:"app_client_private_key"` + RedirectURI string `json:"app_redirect_uri"` + WebHookURI string `json:"app_web_hook_uri"` } // MetricsReport keeps the config needed to send the metrics data report diff --git a/cla-backend-go/config/local.go b/cla-backend-go/config/local.go index bb5d3a324..901b217d8 100644 --- a/cla-backend-go/config/local.go +++ b/cla-backend-go/config/local.go @@ -5,18 +5,25 @@ package config import ( "encoding/json" - "io/ioutil" + "os" "path/filepath" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/sirupsen/logrus" ) func loadLocalConfig(configFilePath string) (Config, error) { - configData, err := ioutil.ReadFile(filepath.Clean(configFilePath)) + f := logrus.Fields{ + "functionName": "config.local.loadLocalConfig", + } + content, err := os.ReadFile(filepath.Clean(configFilePath)) if err != nil { + log.WithFields(f).WithError(err).Warnf("Failed to read config file: %s", configFilePath) return Config{}, err } localConfig := Config{} - err = json.Unmarshal(configData, &localConfig) + err = json.Unmarshal(content, &localConfig) if err != nil { return Config{}, err } diff --git a/cla-backend-go/config/ssm.go b/cla-backend-go/config/ssm.go index 8138989e7..8a5201b9e 100644 --- a/cla-backend-go/config/ssm.go +++ b/cla-backend-go/config/ssm.go @@ -25,7 +25,6 @@ type configLookupResponse struct { // getSSMString is a generic routine to fetch the specified key value func getSSMString(ssmClient *ssm.SSM, key string) (string, error) { - // log.Debugf("Loading SSM parameter: %s", key) value, err := ssmClient.GetParameter(&ssm.GetParameterInput{ Name: aws.String(key), WithDecryption: aws.Bool(false), @@ -45,6 +44,7 @@ func loadSSMConfig(awsSession *session.Session, stage string) Config { //nolint "stage": stage, } config := Config{} + config.SignatureQueryDefaultValue = "all" ssmClient := ssm.New(awsSession) @@ -65,8 +65,20 @@ func loadSSMConfig(awsSession *session.Session, stage string) Config { //nolint fmt.Sprintf("cla-gh-access-token-%s", stage), fmt.Sprintf("cla-gh-app-id-%s", stage), fmt.Sprintf("cla-gh-app-private-key-%s", stage), + fmt.Sprintf("cla-gh-test-organization-%s", stage), + fmt.Sprintf("cla-gh-test-organization-installation-id-%s", stage), + fmt.Sprintf("cla-gh-test-repository-%s", stage), + fmt.Sprintf("cla-gh-test-repository-id-%s", stage), + //fmt.Sprintf("cla-gitlab-oauth-secret-go-backend-%s", stage), + fmt.Sprintf("cla-gitlab-app-id-%s", stage), + fmt.Sprintf("cla-gitlab-app-secret-%s", stage), + fmt.Sprintf("cla-gitlab-app-private-key-%s", stage), + fmt.Sprintf("cla-gitlab-app-redirect-uri-%s", stage), + fmt.Sprintf("cla-gitlab-app-web-hook-uri-%s", stage), fmt.Sprintf("cla-corporate-base-%s", stage), + fmt.Sprintf("cla-corporate-v1-base-%s", stage), fmt.Sprintf("cla-corporate-v2-base-%s", stage), + fmt.Sprintf("cla-contributor-v2-base-%s", stage), fmt.Sprintf("cla-doc-raptor-api-key-%s", stage), fmt.Sprintf("cla-session-store-table-%s", stage), fmt.Sprintf("cla-ses-sender-email-address-%s", stage), @@ -88,6 +100,13 @@ func loadSSMConfig(awsSession *session.Session, stage string) Config { //nolint fmt.Sprintf("cla-lfx-metrics-report-sqs-region-%s", stage), fmt.Sprintf("cla-lfx-metrics-report-sqs-url-%s", stage), fmt.Sprintf("cla-lfx-metrics-report-enabled-%s", stage), + fmt.Sprintf("cla-enable-services-for-parent-%s", stage), + fmt.Sprintf("cla-signature-query-default-%s", stage), + fmt.Sprintf("cla-platform-api-gw-%s", stage), + fmt.Sprintf("cla-api-v4-base-%s", stage), + fmt.Sprintf("cla-landing-page-%s", stage), + fmt.Sprintf("cla-logo-url-%s", stage), + fmt.Sprintf("cla-docusign-private-key-%s", stage), } // For each key to lookup @@ -118,27 +137,58 @@ func loadSSMConfig(awsSession *session.Session, stage string) Config { //nolint case fmt.Sprintf("cla-auth0-algorithm-%s", stage): config.Auth0.Algorithm = resp.value case fmt.Sprintf("cla-gh-oauth-client-id-go-backend-%s", stage): - config.Github.ClientID = resp.value + config.GitHub.ClientID = resp.value case fmt.Sprintf("cla-gh-oauth-secret-go-backend-%s", stage): - config.Github.ClientSecret = resp.value + config.GitHub.ClientSecret = resp.value case fmt.Sprintf("cla-gh-access-token-%s", stage): - config.Github.AccessToken = resp.value + config.GitHub.AccessToken = resp.value case fmt.Sprintf("cla-gh-app-id-%s", stage): githubAppID, err := strconv.Atoi(resp.value) if err != nil { errMsg := fmt.Sprintf("invalid value of key: %s", fmt.Sprintf("cla-gh-app-id-%s", stage)) log.WithFields(f).WithError(err).Fatal(errMsg) } - config.Github.AppID = githubAppID + config.GitHub.AppID = githubAppID case fmt.Sprintf("cla-gh-app-private-key-%s", stage): - config.Github.AppPrivateKey = resp.value + config.GitHub.AppPrivateKey = resp.value + case fmt.Sprintf("cla-gh-test-organization-%s", stage): + config.GitHub.TestOrganization = resp.value + case fmt.Sprintf("cla-gh-test-organization-installation-id-%s", stage): + config.GitHub.TestOrganizationInstallationID = resp.value + case fmt.Sprintf("cla-gh-test-repository-%s", stage): + config.GitHub.TestRepository = resp.value + case fmt.Sprintf("cla-gh-test-repository-id-%s", stage): + config.GitHub.TestRepositoryID = resp.value + // gitlab ssm + case fmt.Sprintf("cla-gitlab-app-id-%s", stage): + config.Gitlab.AppClientID = resp.value + // DEBUG + log.WithFields(f).Debugf("CLA GitLab App ID: %s...%s", resp.value[0:4], resp.value[len(resp.value)-4:]) + case fmt.Sprintf("cla-gitlab-app-secret-%s", stage): + config.Gitlab.AppClientSecret = resp.value + // DEBUG + log.WithFields(f).Debugf("CLA GitLab App Secret: %s...%s", resp.value[0:4], resp.value[len(resp.value)-4:]) + case fmt.Sprintf("cla-gitlab-app-private-key-%s", stage): + config.Gitlab.AppPrivateKey = resp.value + // DEBUG + log.WithFields(f).Debugf("CLA GitLab App Private Key: %s...%s", resp.value[0:4], resp.value[len(resp.value)-4:]) + case fmt.Sprintf("cla-gitlab-app-redirect-uri-%s", stage): + config.Gitlab.RedirectURI = resp.value + case fmt.Sprintf("cla-gitlab-app-web-hook-uri-%s", stage): + config.Gitlab.WebHookURI = resp.value + case fmt.Sprintf("cla-contributor-v2-base-%s", stage): + config.CLAContributorv2Base = resp.value + case fmt.Sprintf("cla-api-v4-base-%s", stage): + config.ClaAPIV4Base = resp.value + case fmt.Sprintf("cla-landing-page-%s", stage): + config.CLALandingPage = resp.value + case fmt.Sprintf("cla-logo-url-%s", stage): + config.CLALogoURL = resp.value case fmt.Sprintf("cla-corporate-base-%s", stage): - corporateConsoleURLValue := resp.value - if corporateConsoleURLValue == "corporate.prod.lfcla.com" { - corporateConsoleURLValue = "corporate.lfcla.com" - } - config.CorporateConsoleURL = corporateConsoleURLValue + config.CorporateConsoleURL = resp.value + case fmt.Sprintf("cla-corporate-v1-base-%s", stage): + config.CorporateConsoleV1URL = resp.value case fmt.Sprintf("cla-corporate-v2-base-%s", stage): config.CorporateConsoleV2URL = resp.value case fmt.Sprintf("cla-doc-raptor-api-key-%s", stage): @@ -170,6 +220,8 @@ func loadSSMConfig(awsSession *session.Session, stage string) Config { //nolint config.Auth0Platform.URL = resp.value case fmt.Sprintf("cla-auth0-platform-api-gw-%s", stage): config.APIGatewayURL = resp.value + case fmt.Sprintf("cla-platform-api-gw-%s", stage): + config.PlatformAPIGatewayURL = resp.value case fmt.Sprintf("cla-lf-group-client-id-%s", stage): config.LFGroup.ClientID = resp.value case fmt.Sprintf("cla-lf-group-client-secret-%s", stage): @@ -197,6 +249,23 @@ func loadSSMConfig(awsSession *session.Session, stage string) Config { //nolint } else { config.MetricsReport.Enabled = boolVal } + case fmt.Sprintf("cla-enable-services-for-parent-%s", stage): + boolVal, err := strconv.ParseBool(resp.value) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to convert %s value to a boolean - setting value to false in the configuration", + fmt.Sprintf("cla-enable-services-for-parent-%s", stage)) + config.EnableCLAServiceForParent = false + } else { + config.EnableCLAServiceForParent = boolVal + } + case fmt.Sprintf("cla-signature-query-default-%s", stage): + if resp.value == "" { + config.SignatureQueryDefault = config.SignatureQueryDefaultValue + } else { + config.SignatureQueryDefault = resp.value + } + case fmt.Sprintf("cla-docusign-private-key-%s", stage): + config.DocuSignPrivateKey = resp.value } } diff --git a/cla-backend-go/docraptor/client.go b/cla-backend-go/docraptor/client.go index 419ec26e5..f7df123c0 100644 --- a/cla-backend-go/docraptor/client.go +++ b/cla-backend-go/docraptor/client.go @@ -49,8 +49,10 @@ func NewDocraptorClient(key string, testMode bool) (Client, error) { // CreatePDF accepts an HTML document and returns a PDF func (dc Client) CreatePDF(html string, claType string) (io.ReadCloser, error) { f := logrus.Fields{ - "functionName": "CreatePDF", + "functionName": "v1.docraptor.client.CreatePDF", "claType": claType, + "testMode": dc.testMode, + "url": dc.url, } document := map[string]interface{}{ @@ -69,9 +71,22 @@ func (dc Client) CreatePDF(html string, claType string) (io.ReadCloser, error) { log.WithFields(f).Debug("Generating PDF using docraptor...") resp, err := http.Post(dc.url, "application/json", bytes.NewBuffer(documentBytes)) if err != nil { - log.WithFields(f).Warnf("problem with API call to docraptor, error: %+v", err) + log.WithFields(f).WithError(err).Warnf("problem with API call to docraptor url: %s", dc.url) return nil, err } + // Do not close - rely on the caller to close the reader otherwise we will get the read from Response.Body after Close error + //defer func() { + // closeErr := resp.Body.Close() + // if closeErr != nil { + // log.WithFields(f).WithError(closeErr).Warn("problem closing docraptor response") + // } + //}() + if resp.StatusCode < 200 || resp.StatusCode > 299 { + msg := fmt.Sprintf("unexpected http response code from docraptor url: %s, status code: %d", dc.url, resp.StatusCode) + log.WithFields(f).Warn(msg) + return nil, errors.New(msg) + } + log.WithFields(f).Debugf("successful response from docraptor url: %s, status code: %d", dc.url, resp.StatusCode) return resp.Body, nil } diff --git a/cla-backend-go/docs/handlers.go b/cla-backend-go/docs/handlers.go index 4bcdf5cd4..1ccae22d1 100644 --- a/cla-backend-go/docs/handlers.go +++ b/cla-backend-go/docs/handlers.go @@ -4,8 +4,8 @@ package docs import ( - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/docs" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/docs" "github.com/go-openapi/runtime/middleware" ) diff --git a/cla-backend-go/docs/swagger.go b/cla-backend-go/docs/swagger.go index d486fa1e9..999b937b8 100644 --- a/cla-backend-go/docs/swagger.go +++ b/cla-backend-go/docs/swagger.go @@ -6,7 +6,7 @@ package docs import ( "net/http" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi" "github.com/go-openapi/runtime" ) diff --git a/cla-backend-go/emails/approval_list_templates.go b/cla-backend-go/emails/approval_list_templates.go new file mode 100644 index 000000000..d677e8cc3 --- /dev/null +++ b/cla-backend-go/emails/approval_list_templates.go @@ -0,0 +1,133 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package emails + +import ( + "errors" + + "github.com/communitybridge/easycla/cla-backend-go/utils" +) + +// ApprovalListRejectedTemplateParams is email params for ApprovalListRejectedTemplate +type ApprovalListRejectedTemplateParams struct { + CommonEmailParams + CLAGroupTemplateParams + CLAManagers []ClaManagerInfoParams +} + +const ( + // ApprovalListRejectedTemplateName is email template name for ApprovalListRejectedTemplate + ApprovalListRejectedTemplateName = "ApprovalListRejectedTemplate" + // ApprovalListRejectedTemplate is email template for + ApprovalListRejectedTemplate = ` +Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the project {{.Project.ExternalProjectName}}.
+Your request to get added to the approval list from {{.CompanyName}} for {{.Project.ExternalProjectName}} was denied by one of the existing CLA Managers. +If you have further questions about this denial, please contact one of the existing CLA Managers from +{{.CompanyName}} for {{.CompanyName}}:
+Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the CLA Group {{.CLAGroupName}}.
+You have been added to the Approval list of {{.CompanyName}} for {{.CLAGroupName}} by CLA Manager {{.Approver}}. +
This means that you are authorized to contribute to the any of the following project(s) associated with the CLA Group {{.CLAGroupName}}: {{.GetProjectsOrProject}}
+If you had previously submitted a pull request to any any the above project(s) that had failed, you can now go back to it and follow the link to verify with your organization.
+ ` +) + +// RenderApprovalListTemplate renders RenderApprovalListTemplate +func RenderApprovalListTemplate(svc EmailTemplateService, projectSFIDs []string, params ApprovalListApprovedTemplateParams) (string, error) { + if len(projectSFIDs) == 0 { + return "", errors.New("projectSFIDs list is empty") + } + + // prefill the projects data + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromProjectSFID(utils.V2, projectSFIDs[0]) + if err != nil { + return "", err + } + params.CLAGroupTemplateParams = claGroupParams + + return RenderTemplate(utils.V2, ApprovalListApprovedTemplateName, ApprovalListApprovedTemplate, params) +} + +// RequestToAuthorizeTemplateParams is email params for RequestToAuthorizeTemplate +type RequestToAuthorizeTemplateParams struct { + CommonEmailParams + // This field is prefilled most of the time with EmailService + CLAGroupTemplateParams + CLAManagers []ClaManagerInfoParams + ContributorName string + ContributorEmail string + OptionalMessage string + CompanyID string +} + +const ( + // RequestToAuthorizeTemplateName is email template name for RequestToAuthorizeTemplate + RequestToAuthorizeTemplateName = "RequestToAuthorizeTemplate" + // RequestToAuthorizeTemplate is email template for + RequestToAuthorizeTemplate = ` +Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the project {{.GetProjectNameOrFoundation}} and CLA Group {{.CLAGroupName}}.
+{{.ContributorName}} ({{.ContributorEmail}}) has requested to be added to the Approved List as an authorized contributor from +{{.CompanyName}} to the project {{.Project.ExternalProjectName}}. You are receiving this message as a CLA Manager from {{.CompanyName}} for +{{.Project.ExternalProjectName}}.
+{{if .OptionalMessage}} +{{.ContributorName}} included the following message in the request:
+{{.OptionalMessage}}
If you want to add them to the Approved List, please +log into the EasyCLA Corporate +Console, where you can approve this user's request by selecting the 'Manage Approved List' and adding the +contributor's email, the contributor's entire email domain, their GitHub ID or the entire GitHub Organization for the +repository. This will permit them to begin contributing to {{.Project.ExternalProjectName}} on behalf of {{.CompanyName}}.
+If you are not certain whether to add them to the Approved List, please reach out to them directly to discuss.
+` +) + +// RenderRequestToAuthorizeTemplate renders RequestToAuthorizeTemplate +func RenderRequestToAuthorizeTemplate(svc EmailTemplateService, claGroupVersion string, projectSFID string, params RequestToAuthorizeTemplateParams) (string, error) { + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromProjectSFID(claGroupVersion, projectSFID) + if err != nil { + return "", err + } + + // assign the prefilled struct + params.CLAGroupTemplateParams = claGroupParams + return RenderTemplate(claGroupVersion, RequestToAuthorizeTemplateName, RequestToAuthorizeTemplate, params) +} diff --git a/cla-backend-go/emails/approval_list_templates_test.go b/cla-backend-go/emails/approval_list_templates_test.go new file mode 100644 index 000000000..2c35ad419 --- /dev/null +++ b/cla-backend-go/emails/approval_list_templates_test.go @@ -0,0 +1,96 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package emails + +import ( + "testing" + + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/stretchr/testify/assert" +) + +func TestApprovalListRejectedTemplate(t *testing.T) { + params := ApprovalListRejectedTemplateParams{ + CommonEmailParams: CommonEmailParams{ + RecipientName: "JohnsClaManager", + CompanyName: "JohnsCompany", + }, + CLAGroupTemplateParams: CLAGroupTemplateParams{ + Projects: []CLAProjectParams{{ExternalProjectName: "JohnsProject"}}, + }, + CLAManagers: []ClaManagerInfoParams{ + {LfUsername: "LFUserName", Email: "LFEmail"}, + }, + } + + result, err := RenderTemplate(utils.V1, ApprovalListRejectedTemplateName, ApprovalListRejectedTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello JohnsClaManager") + assert.Contains(t, result, "regarding the project JohnsProject") + assert.Contains(t, result, "approval list from JohnsCompany for JohnsProject") + assert.Contains(t, result, "OptionalMessageValue
Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the project {{.GetProjectNameOrFoundation}} and CLA Group {{.CLAGroupName}}.
+You have been removed as a CLA Manager from {{.CompanyName}} for the project {{.Project.ExternalProjectName}}.
+If you have further questions about this, please contact one of the existing managers from +{{.CompanyName}}:
+Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the project {{.Project.ExternalProjectName}}.
+You are currently listed as a CLA Manager from {{.CompanyName}} for the project {{.Project.ExternalProjectName}}. This means that you are able to maintain the +list of employees allowed to contribute to {{.Project.ExternalProjectName}} on behalf of your company, as well as view and manage the list of +your company’s CLA Managers for {{.Project.ExternalProjectName}}.
+{{.RequesterName}} ({{.RequesterEmail}}) has requested to be added as another CLA Manager from {{.CompanyName}} for {{.Project.ExternalProjectName}}. This would permit them to maintain the +lists of approved contributors and CLA Managers as well.
+If you want to permit this, please log into the EasyCLA Corporate Console, +select your company, then select the {{.Project.ExternalProjectName}} project. From the CLA Manager requests, you can approve this user as an +additional CLA Manager.
+` +) + +// RenderRequestAccessToCLAManagersTemplate renders the RemovedCLAManagerTemplate +func RenderRequestAccessToCLAManagersTemplate(svc EmailTemplateService, claGroupModelVersion, projectSFID string, params RequestAccessToCLAManagersTemplateParams) (string, error) { + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromProjectSFID(claGroupModelVersion, projectSFID) + if err != nil { + return "", err + } + params.CLAGroupTemplateParams = claGroupParams + + return RenderTemplate(claGroupModelVersion, RequestAccessToCLAManagersTemplateName, RequestAccessToCLAManagersTemplate, params) +} + +// RequestApprovedToCLAManagersTemplateParams is email params for RequestApprovedToCLAManagersTemplate +type RequestApprovedToCLAManagersTemplateParams struct { + CommonEmailParams + CLAGroupTemplateParams + RequesterName string + RequesterEmail string +} + +const ( + // RequestApprovedToCLAManagersTemplateName is email template name for RequestApprovedToCLAManagersTemplate + RequestApprovedToCLAManagersTemplateName = "RequestApprovedToCLAManagersTemplateName" + // RequestApprovedToCLAManagersTemplate is email template for + RequestApprovedToCLAManagersTemplate = ` +Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the project {{.Project.ExternalProjectName}}.
+The following user has been approved as a CLA Manager from {{.CompanyName}} for the project {{.Project.ExternalProjectName}}. This means that they can now +maintain the list of employees allowed to contribute to {{.Project.ExternalProjectName}} on behalf of your company, as well as view and manage the +list of company’s CLA Managers for {{.Project.ExternalProjectName}}.
+Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the project {{.Project.ExternalProjectName}}.
+You have now been approved as a CLA Manager from {{.CompanyName}} for the project {{.Project.ExternalProjectName}}. This means that you can now maintain the +list of employees allowed to contribute to {{.Project.ExternalProjectName}} on behalf of your company, as well as view and manage the list of your +company’s CLA Managers for {{.Project.ExternalProjectName}}.
+To get started, please log into the EasyCLA Corporate Console, and select your +company and then the project {{.Project.ExternalProjectName}}. From here you will be able to edit the list of approved employees and CLA Managers.
+` +) + +// RenderRequestApprovedToRequesterTemplate renders the RemovedCLAManagerTemplate +func RenderRequestApprovedToRequesterTemplate(svc EmailTemplateService, claGroupModelVersion, projectSFID string, params RequestApprovedToRequesterTemplateParams) (string, error) { + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromProjectSFID(claGroupModelVersion, projectSFID) + if err != nil { + return "", err + } + params.CLAGroupTemplateParams = claGroupParams + + return RenderTemplate(claGroupModelVersion, RequestApprovedToRequesterTemplateName, RequestApprovedToRequesterTemplate, params) +} + +// RequestDeniedToCLAManagersTemplateParams is email params for RequestDeniedToCLAManagersTemplate +type RequestDeniedToCLAManagersTemplateParams struct { + CommonEmailParams + CLAGroupTemplateParams + RequesterName string + RequesterEmail string +} + +const ( + // RequestDeniedToCLAManagersTemplateName is email template name for RequestDeniedToCLAManagersTemplate + RequestDeniedToCLAManagersTemplateName = "RequestDeniedToCLAManagersTemplate" + // RequestDeniedToCLAManagersTemplate is email template for + RequestDeniedToCLAManagersTemplate = ` +Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the project {{.Project.ExternalProjectName}}.
+The following user has been denied as a CLA Manager from {{.CompanyName}} for the project {{.Project.ExternalProjectName}}. This means that they will not +be able to maintain the list of employees allowed to contribute to {{.Project.ExternalProjectName}} on behalf of your company.
+Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the project {{.Project.ExternalProjectName}}.
+You have been denied as a CLA Manager from {{.CompanyName}} for the project {{.Project.ExternalProjectName}}. This means that you can not maintain the +list of employees allowed to contribute to {{.Project.ExternalProjectName}} on behalf of your company.
+` +) + +// RenderRequestDeniedToRequesterTemplate renders the RemovedCLAManagerTemplate +func RenderRequestDeniedToRequesterTemplate(svc EmailTemplateService, claGroupModelVersion, projectSFID string, params RequestDeniedToRequesterTemplateParams) (string, error) { + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromProjectSFID(claGroupModelVersion, projectSFID) + if err != nil { + return "", err + } + params.CLAGroupTemplateParams = claGroupParams + + return RenderTemplate(claGroupModelVersion, RequestDeniedToRequesterTemplateName, RequestDeniedToRequesterTemplate, params) +} + +// ClaManagerAddedEToUserTemplateParams is email params +type ClaManagerAddedEToUserTemplateParams struct { + CommonEmailParams + CLAGroupTemplateParams +} + +const ( + // ClaManagerAddedEToUserTemplateName is template name of ClaManagerAddedEToUserTemplate + ClaManagerAddedEToUserTemplateName = "V2ClaManagerAddedEToUserTemplate" + //ClaManagerAddedEToUserTemplate email template for cla manager v2 + ClaManagerAddedEToUserTemplate = ` +Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the project {{.Project.ExternalProjectName}} and CLA Group {{.CLAGroupName}}.
+You have been added as a CLA Manager for the organization {{.CompanyName}} and the project {{.Project.ExternalProjectName}}. This means that you can now maintain the +list of employees allowed to contribute to the project {{.Project.ExternalProjectName}} on behalf of your company, as well as view and manage the list of your +company’s CLA Managers for the CLA Group {{.CLAGroupName}}.
+To get started, please log into the EasyCLA Corporate Console, and select your +company and then the project {{.Project.ExternalProjectName}}. From here you will be able to edit the list of approved employees and CLA Managers.
+` +) + +// RenderClaManagerAddedEToUserTemplate renders the RemovedCLAManagerTemplate +func RenderClaManagerAddedEToUserTemplate(svc EmailTemplateService, claGroupModelVersion, projectSFID string, params ClaManagerAddedEToUserTemplateParams) (string, error) { + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromProjectSFID(claGroupModelVersion, projectSFID) + if err != nil { + return "", err + } + params.CLAGroupTemplateParams = claGroupParams + + return RenderTemplate(claGroupModelVersion, ClaManagerAddedEToUserTemplateName, ClaManagerAddedEToUserTemplate, params) +} + +// ClaManagerAddedToCLAManagersTemplateParams is email params for ClaManagerAddedToCLAManagersTemplate +type ClaManagerAddedToCLAManagersTemplateParams struct { + CommonEmailParams + CLAGroupTemplateParams + Name string + Email string +} + +const ( + // ClaManagerAddedToCLAManagersTemplateName is email template name for ClaManagerAddedToCLAManagersTemplate + ClaManagerAddedToCLAManagersTemplateName = "ClaManagerAddedToCLAManagersTemplate" + // ClaManagerAddedToCLAManagersTemplate is email template for + ClaManagerAddedToCLAManagersTemplate = ` +Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the project {{.Project.ExternalProjectName}} associated with the CLA Group {{.CLAGroupName}}.
+The following user has been added as a CLA Manager from {{.CompanyName}} for the project {{.Project.ExternalProjectName}}. This means that they can now +maintain the list of employees allowed to contribute to {{.Project.ExternalProjectName}} on behalf of your company, as well as view and manage the +list of company’s CLA Managers for {{.Project.ExternalProjectName}}.
+Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the project {{.Project.ExternalProjectName}}.
+{{.Name}} ({{.Email}}) has been removed as a CLA Manager from {{.CompanyName}} for the project {{.Project.ExternalProjectName}}.
+` +) + +// RenderClaManagerDeletedToCLAManagersTemplate renders the RemovedCLAManagerTemplate +func RenderClaManagerDeletedToCLAManagersTemplate(svc EmailTemplateService, claGroupModelVersion, projectSFID string, params ClaManagerDeletedToCLAManagersTemplateParams) (string, error) { + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromProjectSFID(claGroupModelVersion, projectSFID) + if err != nil { + return "", err + } + params.CLAGroupTemplateParams = claGroupParams + + return RenderTemplate(claGroupModelVersion, ClaManagerDeletedToCLAManagersTemplateName, ClaManagerDeletedToCLAManagersTemplate, params) +} diff --git a/cla-backend-go/emails/cla_manager_templates_test.go b/cla-backend-go/emails/cla_manager_templates_test.go new file mode 100644 index 000000000..f401af7dc --- /dev/null +++ b/cla-backend-go/emails/cla_manager_templates_test.go @@ -0,0 +1,256 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package emails + +import ( + "testing" + + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/stretchr/testify/assert" +) + +func TestRemovedCLAManagerTemplate(t *testing.T) { + params := RemovedCLAManagerTemplateParams{ + CommonEmailParams: CommonEmailParams{ + RecipientName: "JohnsClaManager", + CompanyName: "JohnsCompany", + }, + CLAGroupTemplateParams: CLAGroupTemplateParams{ + Projects: []CLAProjectParams{ + {ExternalProjectName: "JohnsProjectExternal"}}, + CLAGroupName: "JohnsProject", + }, + CLAManagers: []ClaManagerInfoParams{ + {LfUsername: "LFUserName", Email: "LFEmail"}, + }, + } + + result, err := RenderTemplate(utils.V1, RemovedCLAManagerTemplateName, RemovedCLAManagerTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello JohnsClaManager") + assert.Contains(t, result, "regarding the project JohnsProject") + assert.Contains(t, result, "CLA Manager from JohnsCompany for the project JohnsProject") + assert.Contains(t, result, "Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the project {{.Project.ExternalProjectName}}.
+The CLA has now been signed. You can download the signed CLA as a PDF here.
+ ` + + DocumentSignedCCLATemplate = ` +Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the project {{.Project.ExternalProjectName}}.
+The CLA has now been signed. You can download the signed CLA as a PDF here, or from within the EasyCLA CLA Manager console .
+ ` +) + +// RenderDocumentSignedTemplate renders RenderDocumentSignedTemplate +func RenderDocumentSignedTemplate(svc EmailTemplateService, claGroupModelVersion, projectSFID string, params DocumentSignedTemplateParams) (string, error) { + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromProjectSFID(claGroupModelVersion, projectSFID) + if err != nil { + return "", err + } + + params.CLAGroupTemplateParams = claGroupParams + var template string + if params.ICLA { + template = DocumentSignedICLATemplate + } else { + template = DocumentSignedCCLATemplate + } + + return RenderTemplate(claGroupModelVersion, DocumentSignedTemplateName, template, params) +} diff --git a/cla-backend-go/emails/github_actions.go b/cla-backend-go/emails/github_actions.go new file mode 100644 index 000000000..e28140b8a --- /dev/null +++ b/cla-backend-go/emails/github_actions.go @@ -0,0 +1,144 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package emails + +// GithubRepositoryActionTemplateParams is email params for GithubRepositoryActionTemplate +type GithubRepositoryActionTemplateParams struct { + CommonEmailParams + CLAGroupTemplateParams + RepositoryName string +} + +// GithubRepositoryDisabledTemplateParams is email params for GithubRepositoryDisabledTemplate +type GithubRepositoryDisabledTemplateParams struct { + GithubRepositoryActionTemplateParams + GithubAction string +} + +const ( + // GithubRepositoryDisabledTemplateName is email template name for GithubRepositoryDisabledTemplate + GithubRepositoryDisabledTemplateName = "GithubRepositoryDisabledTemplate" + // GithubRepositoryDisabledTemplate is email template for + GithubRepositoryDisabledTemplate = ` +Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the Github Repository {{.RepositoryName}} associated with the CLA Group {{.CLAGroupName}}.
+EasyCLA was notified that the Github Repository {{.RepositoryName}} was {{.GithubAction}} from Github. It's now disabled from EasyCLA platform.
+` +) + +// RenderGithubRepositoryDisabledTemplate renders GithubRepositoryDisabledTemplate +func RenderGithubRepositoryDisabledTemplate(svc EmailTemplateService, claGroupID string, params GithubRepositoryDisabledTemplateParams) (string, error) { + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromCLAGroup(claGroupID) + if err != nil { + return "", err + } + + // assign the prefilled struct + params.CLAGroupTemplateParams = claGroupParams + return RenderTemplate(params.CLAGroupTemplateParams.Version, GithubRepositoryDisabledTemplateName, GithubRepositoryDisabledTemplate, params) +} + +// GithubRepositoryArchivedTemplateParams renders GithubRepositoryArchivedTemplate +type GithubRepositoryArchivedTemplateParams struct { + GithubRepositoryActionTemplateParams +} + +const ( + // GithubRepositoryArchivedTemplateName is email template name for GithubRepositoryArchivedTemplate + GithubRepositoryArchivedTemplateName = "GithubRepositoryArchivedTemplate" + // GithubRepositoryArchivedTemplate is email template for + GithubRepositoryArchivedTemplate = ` +Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the Github Repository {{.RepositoryName}} associated with the CLA Group {{.CLAGroupName}}.
+EasyCLA was notified that the Github Repository {{.RepositoryName}} was archived from Github. No action was taken on EasyCLA platform.
+` +) + +// RenderGithubRepositoryArchivedTemplate renders GithubRepositoryArchivedTemplate +func RenderGithubRepositoryArchivedTemplate(svc EmailTemplateService, claGroupID string, params GithubRepositoryArchivedTemplateParams) (string, error) { + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromCLAGroup(claGroupID) + if err != nil { + return "", err + } + + // assign the prefilled struct + params.CLAGroupTemplateParams = claGroupParams + return RenderTemplate(params.CLAGroupTemplateParams.Version, GithubRepositoryArchivedTemplateName, GithubRepositoryArchivedTemplate, params) +} + +// GithubRepositoryRenamedTemplateParams is email params for GithubRepositoryRenamedTemplate +type GithubRepositoryRenamedTemplateParams struct { + GithubRepositoryActionTemplateParams + OldRepositoryName string + NewRepositoryName string +} + +const ( + // GithubRepositoryRenamedTemplateName is email template name for GithubRepositoryRenamedTemplate + GithubRepositoryRenamedTemplateName = "GithubRepositoryRenamedTemplate" + // GithubRepositoryRenamedTemplate is email template for + GithubRepositoryRenamedTemplate = ` +Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the Github Repository {{.RepositoryName}} associated with the CLA Group {{.CLAGroupName}}.
+EasyCLA was notified that the Github Repository {{.OldRepositoryName}} was renamed to {{.NewRepositoryName}} from Github. The change was reflected to EasyCLA platform.
+` +) + +// RenderGithubRepositoryRenamedTemplate renders GithubRepositoryRenamedTemplate +func RenderGithubRepositoryRenamedTemplate(svc EmailTemplateService, claGroupID string, params GithubRepositoryRenamedTemplateParams) (string, error) { + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromCLAGroup(claGroupID) + if err != nil { + return "", err + } + + // assign the prefilled struct + params.CLAGroupTemplateParams = claGroupParams + return RenderTemplate(params.CLAGroupTemplateParams.Version, GithubRepositoryRenamedTemplateName, GithubRepositoryRenamedTemplate, params) +} + +// GithubRepositoryTransferredTemplateParams is email params GithubRepositoryTransferredTemplate +type GithubRepositoryTransferredTemplateParams struct { + GithubRepositoryActionTemplateParams + OldGithubOrgName string + NewGithubOrgName string +} + +const ( + // GithubRepositoryTransferredTemplateName is email template name for GithubRepositoryTransferredTemplate + GithubRepositoryTransferredTemplateName = "GithubRepositoryTransferredTemplate" + // GithubRepositoryTransferredTemplate is email template for + GithubRepositoryTransferredTemplate = ` +Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the Github Repository {{.RepositoryName}} associated with the CLA Group {{.CLAGroupName}}.
+EasyCLA was notified that the Github Repository {{.RepositoryName}} was transferred from {{.OldGithubOrgName}} Organization to {{.NewGithubOrgName}} Organization from Github. The change was reflected to EasyCLA platform.
+` +) + +const ( + // GithubRepositoryTransferredFailedTemplateName is email template name for GithubRepositoryTransferredFailedTemplate + GithubRepositoryTransferredFailedTemplateName = "GithubRepositoryTransferredFailedTemplate" + // GithubRepositoryTransferredFailedTemplate is email template for + GithubRepositoryTransferredFailedTemplate = ` +Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the Github Repository {{.RepositoryName}} associated with the CLA Group {{.CLAGroupName}}.
+EasyCLA was notified that the Github Repository {{.RepositoryName}} was transferred from {{.OldGithubOrgName}} Organization to {{.NewGithubOrgName}} Organization from Github.
+However, we detected that EasyCLA is not enabled for the new Github Organization {{.NewGithubOrgName}}. The Github Repository {{.RepositoryName}} is now disabled from EasyCLA platform.
+` +) + +// RenderGithubRepositoryTransferredTemplate renders GithubRepositoryTransferredFailedTemplate or GithubRepositoryTransferredTemplate +func RenderGithubRepositoryTransferredTemplate(svc EmailTemplateService, claGroupID string, params GithubRepositoryTransferredTemplateParams, success bool) (string, error) { + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromCLAGroup(claGroupID) + if err != nil { + return "", err + } + + // assign the prefilled struct + params.CLAGroupTemplateParams = claGroupParams + if success { + return RenderTemplate(params.CLAGroupTemplateParams.Version, GithubRepositoryTransferredTemplateName, GithubRepositoryTransferredTemplate, params) + } + return RenderTemplate(params.CLAGroupTemplateParams.Version, GithubRepositoryTransferredFailedTemplateName, GithubRepositoryTransferredFailedTemplate, params) + +} diff --git a/cla-backend-go/emails/github_actions_test.go b/cla-backend-go/emails/github_actions_test.go new file mode 100644 index 000000000..89418ee98 --- /dev/null +++ b/cla-backend-go/emails/github_actions_test.go @@ -0,0 +1,130 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package emails + +import ( + "testing" + + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/stretchr/testify/assert" +) + +func TestGithubRepositoryDisabledTemplate(t *testing.T) { + params := GithubRepositoryDisabledTemplateParams{ + GithubRepositoryActionTemplateParams: GithubRepositoryActionTemplateParams{ + CommonEmailParams: CommonEmailParams{ + RecipientName: "CLA Manager", + }, + CLAGroupTemplateParams: CLAGroupTemplateParams{ + CLAGroupName: "JohnsProject", + }, + RepositoryName: "johnsRepository", + }, + GithubAction: "deleted", + } + + result, err := RenderTemplate(utils.V2, GithubRepositoryDisabledTemplateName, GithubRepositoryDisabledTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello CLA Manager") + assert.Contains(t, result, "regarding the Github Repository johnsRepository") + assert.Contains(t, result, "associated with the CLA Group JohnsProject") + assert.Contains(t, result, "Github Repository johnsRepository was deleted") +} + +func TestGithubRepositoryArchivedTemplate(t *testing.T) { + params := GithubRepositoryArchivedTemplateParams{ + GithubRepositoryActionTemplateParams: GithubRepositoryActionTemplateParams{ + CommonEmailParams: CommonEmailParams{ + RecipientName: "CLA Manager", + }, + CLAGroupTemplateParams: CLAGroupTemplateParams{ + CLAGroupName: "JohnsProject", + }, + RepositoryName: "johnsRepository", + }, + } + + result, err := RenderTemplate(utils.V2, GithubRepositoryArchivedTemplateName, GithubRepositoryArchivedTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello CLA Manager") + assert.Contains(t, result, "regarding the Github Repository johnsRepository") + assert.Contains(t, result, "associated with the CLA Group JohnsProject") + assert.Contains(t, result, "Github Repository johnsRepository was archived") +} + +func TestGithubRepositoryRenamedTemplate(t *testing.T) { + params := GithubRepositoryRenamedTemplateParams{ + GithubRepositoryActionTemplateParams: GithubRepositoryActionTemplateParams{ + CommonEmailParams: CommonEmailParams{ + RecipientName: "CLA Manager", + }, + CLAGroupTemplateParams: CLAGroupTemplateParams{ + CLAGroupName: "JohnsProject", + }, + RepositoryName: "johnsNewRepository", + }, + OldRepositoryName: "johnsOldRepository", + NewRepositoryName: "johnsNewRepository", + } + + result, err := RenderTemplate(utils.V2, GithubRepositoryRenamedTemplateName, GithubRepositoryRenamedTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello CLA Manager") + assert.Contains(t, result, "regarding the Github Repository johnsNewRepository") + assert.Contains(t, result, "associated with the CLA Group JohnsProject") + assert.Contains(t, result, "Github Repository johnsOldRepository was renamed to johnsNewRepository") +} + +func TestGithubRepositoryTransferredTemplate(t *testing.T) { + params := GithubRepositoryTransferredTemplateParams{ + GithubRepositoryActionTemplateParams: GithubRepositoryActionTemplateParams{ + CommonEmailParams: CommonEmailParams{ + RecipientName: "CLA Manager", + }, + CLAGroupTemplateParams: CLAGroupTemplateParams{ + CLAGroupName: "JohnsProject", + }, + RepositoryName: "johnsNewRepository", + }, + OldGithubOrgName: "johnsOldGithubOrg", + NewGithubOrgName: "johnsNewGithubOrg", + } + + result, err := RenderTemplate(utils.V2, GithubRepositoryTransferredTemplateName, GithubRepositoryTransferredTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello CLA Manager") + assert.Contains(t, result, "regarding the Github Repository johnsNewRepository") + assert.Contains(t, result, "associated with the CLA Group JohnsProject") + assert.Contains(t, result, "Github Repository johnsNewRepository was transferred from johnsOldGithubOrg Organization to johnsNewGithubOrg Organization") +} + +func TestGithubRepositoryTransferredFailedTemplate(t *testing.T) { + params := GithubRepositoryTransferredTemplateParams{ + GithubRepositoryActionTemplateParams: GithubRepositoryActionTemplateParams{ + CommonEmailParams: CommonEmailParams{ + RecipientName: "CLA Manager", + }, + CLAGroupTemplateParams: CLAGroupTemplateParams{ + CLAGroupName: "JohnsProject", + }, + RepositoryName: "johnsNewRepository", + }, + OldGithubOrgName: "johnsOldGithubOrg", + NewGithubOrgName: "johnsNewGithubOrg", + } + + result, err := RenderTemplate(utils.V2, GithubRepositoryTransferredFailedTemplateName, GithubRepositoryTransferredFailedTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello CLA Manager") + assert.Contains(t, result, "regarding the Github Repository johnsNewRepository") + assert.Contains(t, result, "associated with the CLA Group JohnsProject") + assert.Contains(t, result, "Github Repository johnsNewRepository was transferred from johnsOldGithubOrg Organization to johnsNewGithubOrg Organization") + assert.Contains(t, result, "EasyCLA is not enabled for the new Github Organization johnsNewGithubOrg") + assert.Contains(t, result, "The Github Repository johnsNewRepository is now disabled") +} diff --git a/cla-backend-go/emails/params.go b/cla-backend-go/emails/params.go new file mode 100644 index 000000000..ab2b3373f --- /dev/null +++ b/cla-backend-go/emails/params.go @@ -0,0 +1,114 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package emails + +import ( + "fmt" + "html/template" + "net/url" + "path" + "strings" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" +) + +// CommonEmailParams are part of almost every email it's sent from the system +type CommonEmailParams struct { + RecipientName string + RecipientAddress string + CompanyName string +} + +// ClaManagerInfoParams represents the CLAManagerInfo used inside of the Email Templates +type ClaManagerInfoParams struct { + LfUsername string + Email string +} + +// CLAProjectParams is useful struct which keeps cla group project info and also +// know how to render it's corporate console url +type CLAProjectParams struct { + ExternalProjectName string + ProjectSFID string + FoundationName string + FoundationSFID string + SignedAtFoundationLevel bool + IsFoundation bool + CorporateConsole string +} + +// GetProjectFullURL has the logic how to return back it's full url in corporate console +// it checks at SignedAtFoundationLevel flag as well for this specific kind of projects +func (p CLAProjectParams) GetProjectFullURL() template.HTML { + if p.CorporateConsole == "" { + return template.HTML(url.QueryEscape(p.ExternalProjectName)) // nolint gosec auto-escape HTML + } + + u, err := url.Parse(p.CorporateConsole) + if err != nil { + log.Warnf("couldn't parse the console url, probably wrong configuration used : %s : %v", p.CorporateConsole, err) + // at least return the project name so we don't have broken email + return template.HTML(url.QueryEscape(p.ExternalProjectName)) // nolint gosec auto-escape HTML + } + + var projectConsolePathURL string + fullURLHtml := `%s` + if p.IsFoundation { + u.Path = path.Join(u.Path, "foundation", p.FoundationSFID, "cla") + projectConsolePathURL = u.String() + } else { + u.Path = path.Join(u.Path, "foundation", p.FoundationSFID, "project", p.ProjectSFID, "cla") + projectConsolePathURL = u.String() + } + + return template.HTML(fmt.Sprintf(fullURLHtml, projectConsolePathURL, p.ExternalProjectName)) // nolint gosec auto-escape HTML +} + +// CLAGroupTemplateParams includes the params for the CLAGroupTemplateParams +type CLAGroupTemplateParams struct { + CorporateConsole string + CLAGroupName string + // ChildProjectCount indicates how many childProjects are under this CLAGroup + // this is important for some of the email rendering knowing if claGroup has + // multiple children + ChildProjectCount int + Projects []CLAProjectParams + Version string +} + +// GetProjectNameOrFoundation returns if the foundationName is set it gets back +// the foundation Name otherwise the ProjectName is returned +func (claParams CLAGroupTemplateParams) GetProjectNameOrFoundation() string { + project := claParams.Projects[0] + if claParams.ChildProjectCount == 1 { + return claParams.Projects[0].ExternalProjectName + } + + // if multiple return the foundation if present + if project.FoundationName != "" { + return project.FoundationName + } + //default to project name if nothing works + return project.ExternalProjectName +} + +// Project is used generally in v1 templates because the matching there was 1:1 +// it will returns the first element from the projects list +func (claParams CLAGroupTemplateParams) Project() CLAProjectParams { + return claParams.Projects[0] +} + +// GetProjectsOrProject gets the first if single or all of them comma separated +func (claParams CLAGroupTemplateParams) GetProjectsOrProject() string { + if len(claParams.Projects) == 1 { + return claParams.Projects[0].ExternalProjectName + } + + var projectNames []string + for _, p := range claParams.Projects { + projectNames = append(projectNames, p.ExternalProjectName) + } + + return strings.Join(projectNames, ", ") +} diff --git a/cla-backend-go/emails/params_test.go b/cla-backend-go/emails/params_test.go new file mode 100644 index 000000000..346b58eee --- /dev/null +++ b/cla-backend-go/emails/params_test.go @@ -0,0 +1,60 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package emails + +import ( + "html/template" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCLAProjectParams_GetProjectFullURL(t *testing.T) { + testCases := []struct { + name string + projectParams CLAProjectParams + result template.HTML + }{ + { + name: "empty url", + projectParams: CLAProjectParams{ + ExternalProjectName: "JohnsProject", + }, + result: template.HTML("JohnsProject"), + }, + { + name: "foundation level project", + projectParams: CLAProjectParams{ + ExternalProjectName: "JohnsProject", + ProjectSFID: "projectSFIDValue", + FoundationName: "CNCF", + FoundationSFID: "FoundationSFIDValue", + SignedAtFoundationLevel: true, + IsFoundation: true, + CorporateConsole: "https://corporate.dev.lfcla.com", + }, + result: template.HTML(`JohnsProject`), + }, + { + name: "standalone project", + projectParams: CLAProjectParams{ + ExternalProjectName: "JohnsProject", + ProjectSFID: "projectSFIDValue", + FoundationName: "CNCF", + FoundationSFID: "FoundationSFIDValue", + SignedAtFoundationLevel: false, + IsFoundation: false, + CorporateConsole: "https://corporate.dev.lfcla.com", + }, + result: template.HTML(`JohnsProject`), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(tt *testing.T) { + resul := tc.projectParams.GetProjectFullURL() + assert.Equal(tt, tc.result, resul) + }) + } +} diff --git a/cla-backend-go/emails/prefill.go b/cla-backend-go/emails/prefill.go new file mode 100644 index 000000000..501df2e0d --- /dev/null +++ b/cla-backend-go/emails/prefill.go @@ -0,0 +1,186 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package emails + +import ( + "context" + "fmt" + + "github.com/communitybridge/easycla/cla-backend-go/project/repository" + service2 "github.com/communitybridge/easycla/cla-backend-go/project/service" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + + "github.com/communitybridge/easycla/cla-backend-go/utils" + v2ProjectService "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" + + "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" +) + +// EmailTemplateService has utility functions needed to pre-fill the email params +type EmailTemplateService interface { + PrefillV2CLAProjectParams(projectSFIDs []string) ([]CLAProjectParams, error) + GetCLAGroupTemplateParamsFromProjectSFID(claGroupVersion, projectSFID string) (CLAGroupTemplateParams, error) + GetCLAGroupTemplateParamsFromCLAGroup(claGroupID string) (CLAGroupTemplateParams, error) +} + +type emailTemplateServiceProvider struct { + claGroupRepository repository.ProjectRepository + repository projects_cla_groups.Repository + projectService service2.Service + corporateConsoleV1 string + corporateConsoleV2 string +} + +// NewEmailTemplateService creates a new instance of email template service +func NewEmailTemplateService(claGroupRepository repository.ProjectRepository, repository projects_cla_groups.Repository, projectService service2.Service, corporateConsoleV1, corporateConsoleV2 string) EmailTemplateService { + return &emailTemplateServiceProvider{ + claGroupRepository: claGroupRepository, + repository: repository, + projectService: projectService, + corporateConsoleV1: corporateConsoleV1, + corporateConsoleV2: corporateConsoleV2, + } +} + +// PrefillV2CLAProjectParams for each supplied projectSFIDs gets the claGroup info + checks if the project is signed at +// foundation level which is important for email rendering +func (s *emailTemplateServiceProvider) PrefillV2CLAProjectParams(projectSFIDs []string) ([]CLAProjectParams, error) { + if len(projectSFIDs) == 0 { + return nil, nil + } + + var claProjectParams []CLAProjectParams + // keeping a cache so we can safe some of the remote svc calls + signedAtFoundationLevelCache := map[string]bool{} + for _, pSFID := range projectSFIDs { + projectCLAGroup, err := s.repository.GetClaGroupIDForProject(context.Background(), pSFID) + if err != nil { + return nil, fmt.Errorf("fetching project : %s failed: %v", pSFID, err) + } + + params := CLAProjectParams{ + ExternalProjectName: projectCLAGroup.ProjectName, + ProjectSFID: pSFID, + FoundationName: projectCLAGroup.FoundationName, + FoundationSFID: projectCLAGroup.FoundationSFID, + CorporateConsole: s.corporateConsoleV2, + IsFoundation: false, + } + + projectClient := v2ProjectService.GetClient() + projectModel, err := projectClient.GetProject(pSFID) + if err != nil { + log.Warnf("unable to fetch project : %s details from project service : %v", pSFID, err) + return nil, fmt.Errorf("unable to fetch project : %s details from project service : %v", pSFID, err) + } + + isFoundation := utils.IsProjectHasRootParent(projectModel) + params.IsFoundation = isFoundation + + signedResult, err := s.projectService.SignedAtFoundationLevel(context.Background(), projectCLAGroup.FoundationSFID) + if err != nil { + return nil, fmt.Errorf("fetching the SignedAtFoundationLevel for foundation : %s failed : %v", projectCLAGroup.FoundationSFID, err) + } + params.SignedAtFoundationLevel = signedResult + signedAtFoundationLevelCache[projectCLAGroup.FoundationSFID] = signedResult + + claProjectParams = append(claProjectParams, params) + } + + return claProjectParams, nil +} + +// GetCLAGroupTemplateParamsFromProjectSFID creates CLAGroupTemplateParams from projectSFID +func (s *emailTemplateServiceProvider) GetCLAGroupTemplateParamsFromProjectSFID(claGroupVersion, projectSFID string) (CLAGroupTemplateParams, error) { + if utils.V2 == claGroupVersion { + return s.getV2CLAGroupTemplateParamsFromProjectSFID(projectSFID) + } + return s.getV1CLAGroupTemplateParamsFromProjectSFID(projectSFID) +} + +// GetCLAGroupTemplateParamsFromCLAGroup fills up the CLAGroupTemplateParams with the basic information, it's missing the +// project information, if needed can be added later on... +func (s *emailTemplateServiceProvider) GetCLAGroupTemplateParamsFromCLAGroup(claGroupID string) (CLAGroupTemplateParams, error) { + claGroupModel, err := s.claGroupRepository.GetCLAGroupByID(context.Background(), claGroupID, false) + if err != nil { + return CLAGroupTemplateParams{}, err + } + + params := CLAGroupTemplateParams{} + params.CLAGroupName = claGroupModel.ProjectName + params.CorporateConsole = s.corporateConsoleV2 + params.Version = claGroupModel.Version + + return params, nil +} + +func (s *emailTemplateServiceProvider) getV2CLAGroupTemplateParamsFromProjectSFID(projectSFID string) (CLAGroupTemplateParams, error) { + projectCLAGroup, err := s.repository.GetClaGroupIDForProject(context.Background(), projectSFID) + if err != nil { + return CLAGroupTemplateParams{}, err + } + + params := &CLAGroupTemplateParams{} + params.CLAGroupName = projectCLAGroup.ClaGroupName + params.CorporateConsole = s.corporateConsoleV2 + params.Version = projectCLAGroup.Version + + projects, err := s.repository.GetProjectsIdsForClaGroup(context.Background(), projectCLAGroup.ClaGroupID) + if err != nil { + return CLAGroupTemplateParams{}, fmt.Errorf("getProjectsIdsForClaGroup failed : %w", err) + } + + params.ChildProjectCount = len(projects) + var projectSFIDs []string + for _, p := range projects { + projectSFIDs = append(projectSFIDs, p.ProjectSFID) + } + + projectParams, err := s.PrefillV2CLAProjectParams(projectSFIDs) + if err != nil { + return CLAGroupTemplateParams{}, fmt.Errorf("prefilling cla project params failed : %v", err) + } + params.Projects = projectParams + return *params, nil +} + +func (s *emailTemplateServiceProvider) getV1CLAGroupTemplateParamsFromProjectSFID(projectSFID string) (CLAGroupTemplateParams, error) { + claGroup, err := s.claGroupRepository.GetClaGroupByProjectSFID(context.Background(), projectSFID, true) + if err != nil { + return CLAGroupTemplateParams{}, err + } + + ps := v2ProjectService.GetClient() + projectSF, projectErr := ps.GetProject(projectSFID) + if projectErr != nil { + return CLAGroupTemplateParams{}, fmt.Errorf("project service lookup error for SFID: %s, error : %+v", projectSFID, projectErr) + } + + var signedResult bool + if claGroup.FoundationSFID != "" { + signedResult, err = s.projectService.SignedAtFoundationLevel(context.Background(), claGroup.FoundationSFID) + if err != nil { + log.Warnf("fetching the SignedAtFoundationLevel for foundation : %s failed : %v skipping assigning in email params", claGroup.FoundationSFID, err) + } + } + + return CLAGroupTemplateParams{ + CorporateConsole: s.corporateConsoleV1, + CLAGroupName: claGroup.ProjectName, + Version: claGroup.Version, + ChildProjectCount: 1, + Projects: []CLAProjectParams{ + { + ExternalProjectName: projectSF.Name, + ProjectSFID: projectSFID, + FoundationName: projectSF.Foundation.Name, + FoundationSFID: projectSF.Foundation.ID, + SignedAtFoundationLevel: signedResult, + CorporateConsole: s.corporateConsoleV1, + }, + }, + }, nil + +} diff --git a/cla-backend-go/emails/render.go b/cla-backend-go/emails/render.go new file mode 100644 index 000000000..48267e41f --- /dev/null +++ b/cla-backend-go/emails/render.go @@ -0,0 +1,30 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package emails + +import ( + "bytes" + "html/template" + + "github.com/communitybridge/easycla/cla-backend-go/utils" +) + +// RenderTemplate renders the template for given template with given params +func RenderTemplate(claGroupVersion, templateName, templateStr string, params interface{}) (string, error) { + tmpl := template.New(templateName) + t, err := tmpl.Parse(templateStr) + if err != nil { + return "", err + } + + var tpl bytes.Buffer + if err := t.Execute(&tpl, params); err != nil { + return "", err + } + + result := tpl.String() + result = result + utils.GetEmailHelpContent(claGroupVersion == utils.V2) + result = result + utils.GetEmailSignOffContent() + return result, nil +} diff --git a/cla-backend-go/emails/service.go b/cla-backend-go/emails/service.go new file mode 100644 index 000000000..df35878ef --- /dev/null +++ b/cla-backend-go/emails/service.go @@ -0,0 +1,50 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package emails + +import ( + "context" + "fmt" + + service2 "github.com/communitybridge/easycla/cla-backend-go/project/service" + + "github.com/communitybridge/easycla/cla-backend-go/utils" +) + +// Service is a service with some helper functions for rendering templates and also sending emails +type Service interface { + EmailTemplateService + NotifyClaManagersForClaGroupID(ctx context.Context, claGrpoupID, subject, body string) error +} + +type service struct { + EmailTemplateService + claService service2.Service +} + +// NewService is constructor for emails.Service +func NewService(emailTemplateService EmailTemplateService, claService service2.Service) Service { + return &service{ + EmailTemplateService: emailTemplateService, + claService: claService, + } +} + +func (s *service) NotifyClaManagersForClaGroupID(ctx context.Context, claGrpoupID, subject, body string) error { + claManagers, err := s.claService.GetCLAManagers(ctx, claGrpoupID) + if err != nil { + return fmt.Errorf("fetching cla manager for cla group : %s failed : %v", claGrpoupID, err) + } + + if len(claManagers) == 0 { + return fmt.Errorf("no cla managers registered for the claGroup : %s, none to notify", claGrpoupID) + } + + var recipientEmails []string + for _, claManager := range claManagers { + recipientEmails = append(recipientEmails, claManager.UserEmail) + } + + return utils.SendEmail(subject, body, recipientEmails) +} diff --git a/cla-backend-go/emails/v2_cla_manager_templates.go b/cla-backend-go/emails/v2_cla_manager_templates.go new file mode 100644 index 000000000..79ca22040 --- /dev/null +++ b/cla-backend-go/emails/v2_cla_manager_templates.go @@ -0,0 +1,257 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package emails + +import ( + "github.com/communitybridge/easycla/cla-backend-go/utils" +) + +// Contributor representing GH user details +type Contributor struct { + Email string + Username string + EmailLabel string + UsernameLabel string +} + +// V2ContributorApprovalRequestTemplateParams is email template params for V2ContributorApprovalRequestTemplate +type V2ContributorApprovalRequestTemplateParams struct { + CommonEmailParams + CLAGroupTemplateParams + SigningEntityName string + UserDetails string +} + +const ( + // V2ContributorApprovalRequestTemplateName is email template name for V2ContributorApprovalRequestTemplate + V2ContributorApprovalRequestTemplateName = "V2ContributorApprovalRequestTemplateName" + // V2ContributorApprovalRequestTemplate is email template for + V2ContributorApprovalRequestTemplate = ` +Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the organization {{.CompanyName}}.
+The following contributor would like to submit a contribution to the projects(s): {{.GetProjectsOrProject}} and is requesting to be added to the approval list as a contributor for your organization:
+{{.UserDetails}}
+CLA Managers can visit the EasyCLA corporate console page for {{range $index, $projectName := .Projects}}{{if $index}},{{end}}{{$projectName.GetProjectFullURL}}{{end}} and add the contributor to one of the approval lists.
+Please notify the contributor once they are added to the approved list of contributors so that they can complete their contribution.
+` +) + +// RenderV2ContributorApprovalRequestTemplate renders V2ContributorApprovalRequestTemplate +func RenderV2ContributorApprovalRequestTemplate(svc EmailTemplateService, projectSFIDs []string, params V2ContributorApprovalRequestTemplateParams) (string, error) { + + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromProjectSFID(utils.V2, projectSFIDs[0]) + if err != nil { + return "", err + } + params.CLAGroupTemplateParams = claGroupParams + + return RenderTemplate(utils.V2, V2ContributorApprovalRequestTemplateName, V2ContributorApprovalRequestTemplate, params) +} + +// V2OrgAdminTemplateParams is email params for V2OrgAdminTemplate +type V2OrgAdminTemplateParams struct { + CommonEmailParams + CLAGroupTemplateParams + SenderName string + SenderEmail string +} + +const ( + // V2OrgAdminTemplateName is template name for V2OrgAdminTemplate + V2OrgAdminTemplateName = "V2OrgAdminTemplate" + // V2OrgAdminTemplate is email template for + V2OrgAdminTemplate = ` +Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the CLA setup and signing process for the organization {{.CompanyName}}.
+{{.SenderName}} {{.SenderEmail}} has identified you as a potential candidate to setup the Corporate CLA in support of the following project(s):
+Before the contribution can be accepted, your organization must sign a CLA. +Either you or someone whom to designate from your company can login to the EasyCLA portal and sign the CLA for this project {{.Project.GetProjectFullURL}}.
+If you are not the CLA Manager, please forward this email to the appropriate person so that they can start the CLA process.
+Please notify the user once CLA setup is complete.
+` +) + +// RenderV2OrgAdminTemplate renders V2OrgAdminTemplate +func RenderV2OrgAdminTemplate(svc EmailTemplateService, projectSFID string, params V2OrgAdminTemplateParams) (string, error) { + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromProjectSFID(utils.V2, projectSFID) + if err != nil { + return "", err + } + params.CLAGroupTemplateParams = claGroupParams + return RenderTemplate(utils.V2, V2OrgAdminTemplateName, V2OrgAdminTemplate, params) +} + +// V2ContributorToOrgAdminTemplateParams is email template params for V2ContributorToOrgAdminTemplate +type V2ContributorToOrgAdminTemplateParams struct { + CommonEmailParams + CLAGroupTemplateParams + UserDetails string +} + +const ( + // V2ContributorToOrgAdminTemplateName is email template name for V2ContributorToOrgAdminTemplate + V2ContributorToOrgAdminTemplateName = "V2ContributorToOrgAdminTemplate" + // V2ContributorToOrgAdminTemplate is email template for + V2ContributorToOrgAdminTemplate = ` +Hello {{.RecipientName}},
+The following contributor would like to submit a contribution to {{range $index, $projectName := .Projects}}{{if $index}},{{end}}{{$projectName.ExternalProjectName}}{{end}} and is requesting to be added to the approval list as a contributor for your organization:
+{{.UserDetails}}
+Before the contribution can be accepted, your organization must sign a CLA. Either you or someone whom you designate from your company can login to this portal and sign the CLA for any of the project(s): {{range $index, $projectName := .Projects}}{{if $index}},{{end}}{{$projectName.GetProjectFullURL}}{{end}}.
+Please notify the contributor once they are added so that they may complete the contribution process.
+ +` +) + +// RenderV2ContributorToOrgAdminTemplate renders V2ContributorToOrgAdminTemplate +func RenderV2ContributorToOrgAdminTemplate(svc EmailTemplateService, projectSFIDs []string, params V2ContributorToOrgAdminTemplateParams) (string, error) { + // prefill the projects data + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromProjectSFID(utils.V2, projectSFIDs[0]) + if err != nil { + return "", err + } + params.CLAGroupTemplateParams = claGroupParams + + return RenderTemplate(utils.V2, V2ContributorToOrgAdminTemplateName, + V2ContributorToOrgAdminTemplate, params) +} + +// V2CLAManagerDesigneeCorporateTemplateParams is email params for V2CLAManagerDesigneeCorporateTemplate +type V2CLAManagerDesigneeCorporateTemplateParams struct { + CommonEmailParams + CLAGroupTemplateParams + SenderName string + SenderEmail string +} + +const ( + // V2CLAManagerDesigneeCorporateTemplateName is email template name for V2CLAManagerDesigneeCorporateTemplate + V2CLAManagerDesigneeCorporateTemplateName = "V2CLAManagerDesigneeCorporateTemplate" + // V2CLAManagerDesigneeCorporateTemplate is email template for + V2CLAManagerDesigneeCorporateTemplate = ` +Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the CLA setup and signing process for the organization {{.CompanyName}}.
+{{.SenderName}} {{.SenderEmail}} has identified you as a potential candidate to setup the Corporate CLA for the organization {{.CompanyName}} in support of the following project(s):
+Before the contribution can be accepted, your organization must sign a CLA. +Either you or someone whom you designate from your company can login and sign the CLA for this project {{.Project.GetProjectFullURL}}
+If you are not the CLA Manager, please forward this email to the appropriate person so that they can start the CLA process.
+Please notify the user once CLA setup is complete.
+` +) + +// RenderV2CLAManagerDesigneeCorporateTemplate renders V2CLAManagerDesigneeCorporateTemplate +func RenderV2CLAManagerDesigneeCorporateTemplate(emailSvc EmailTemplateService, projectSFID string, params V2CLAManagerDesigneeCorporateTemplateParams) (string, error) { + claGroupParams, err := emailSvc.GetCLAGroupTemplateParamsFromProjectSFID(utils.V2, projectSFID) + if err != nil { + return "", err + } + params.CLAGroupTemplateParams = claGroupParams + + return RenderTemplate(utils.V2, V2CLAManagerDesigneeCorporateTemplateName, V2CLAManagerDesigneeCorporateTemplate, params) +} + +// V2ToCLAManagerDesigneeTemplateParams is email params for V2ToCLAManagerDesigneeTemplate +type V2ToCLAManagerDesigneeTemplateParams struct { + CommonEmailParams + CLAGroupTemplateParams + Contributor Contributor +} + +const ( + // V2ToCLAManagerDesigneeTemplateName is email template name for V2ToCLAManagerDesigneeTemplate + V2ToCLAManagerDesigneeTemplateName = "V2ToCLAManagerDesigneeTemplateName" + // V2ToCLAManagerDesigneeTemplate is email template for + V2ToCLAManagerDesigneeTemplate = ` +Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the project(s): {{.GetProjectsOrProject}}.
+We received a request from {{.Contributor.UsernameLabel}}: {{.Contributor.Username}} ({{.Contributor.EmailLabel}}: {{.Contributor.Email}}) to contribute to the above projects on behalf of your organization.
+Before the user contribution can be accepted, your organization must sign a Corporate CLA (CCLA).The requester has stated that you would be the initial CLA Manager for this CCLA, to coordinate the signing of the CCLA and then manage the list of employees who are authorized to contribute.
+Please complete the following steps:
+Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the project(s): {{.GetProjectsOrProject}}.
+We received a request from {{.Contributor.UsernameLabel}}: {{.Contributor.Username}} ({{.Contributor.EmailLabel}}: {{.Contributor.Email}}) to contribute to any of the above projects on behalf of your +organization {{.CompanyName}}.
+
Before the user contribution can be accepted, your organization must sign a Corporate CLA(CCLA). +The requester has stated that you would be the initial CLA Manager for this CCLA, to coordinate the signing of the CCLA and then manage the list of employees who are authorized to contribute.
+Please complete the following steps:
+Hello {{.RecipientName}},
+This is a notification email from EasyCLA regarding the CLA setup and signing process for the organization {{.CompanyName}}.The user {{.RequesterUserName}} ({{.RequesterEmail}}) has identified you as a potential candidate to setup the Corporate CLA for the organization {{.CompanyName }} and the project {{.GetProjectNameOrFoundation}}
+Before the user contribution can be accepted, your organization must sign a Corporate CLA(CCLA).
+Please complete the following steps:
+Hello {{.RecipientName}}
+This is a notification email from EasyCLA regarding the CLA Group {{.ProjectName}}
+You were previously authorized to contribute on behalf of your company {{COMPANY-NAME}} under its CLA. However, a CLA Manager has now removed you from the authorization list. This has additionally resulted in invalidating your current signed Individual CLA (ICLA).
+As a result, you will no longer be able to contribute until you are again authorized under another signed CLA.
+Please contact one of the CLA Managers from your company if you have questions about why you were removed. The CLA Managers from your company for this CLA are:
+Hello {{.RecipientName}}
+This is a notification email from EasyCLA regarding the CLA Group {{.CLAGroupName}}.
+You were previously authorized to contribute on behalf of your company {{.Company}} under its CLA. However, a CLA Manager {{.ClaManager}} has now removed you from the authorization list.
+As a result, you will no longer be able to contribute until you are again authorized under another signed CLA.
+Please contact one of the CLA Managers from your company if you have questions about why you were removed. The CLA Managers from your company for this CLA are:
+Hello {{.RecipientName}}
+This is a notification email from EasyCLA regarding the CLA Group {{.CLAGroupName}}.
+You had previously signed an Individual CLA (ICLA) to contribute to the project on your own behalf. However, the Project Manager has marked your ICLA as invalidated. This might be because the ICLA may have been signed in error, if your contributions should have been on behalf of your employer rather than on your own behalf.
+As a result, you will no longer be able to contribute until you are again authorized under another signed CLA.
+Please contact the Project Manager for this project if you have questions about why you were removed.
+ + ` + + //InvalidateCCLAICLAECLASignatureTemplateName is email template upon approval list removal for ccla use case + InvalidateCCLAICLAECLASignatureTemplateName = "InvalidateCCLAICLAECLASignatureTemplate" + //InvalidateCCLAICLAECLASignatureTemplate ... + InvalidateCCLAICLAECLASignatureTemplate = ` +Hello {{.RecipientName}}
+This is a notification email from EasyCLA regarding the CLA Group {{.CLAGroupName}}.
+You were previously authorized to contribute on behalf of your company {{.Company}} under its CLA. However, a CLA Manager has now removed you from the authorization list. This has additionally resulted in invalidating your current signed Individual CLA (ICLA) and your acknowledgement.
+As a result, you will no longer be able to contribute until you are again authorized under another signed CLA.
+Please contact one of the CLA Managers from your company if you have questions about why you were removed. The CLA Managers from your company for this CLA are:
+Hello %s,
+This is a notification email from EasyCLA regarding the project %s.
+You have been %s %s the Approval List of %s for %s by CLA Manager %s. This means that %s.
+ +If you are a GitHub user and If you had previously submitted a pull request to EasyCLA Test Group that had failed, you can now go back to it, re-click the “Not Covered” button in the EasyCLA message in your pull request, and then follow these steps
+If you are a Gerrit user and if you had previously submitted a pull request to EasyCLA Test Group that had failed, then navigate to Agreements Settings page on Gerrit, click on "New Contributor Agreement" link under Agreements section, select the radio button corresponding to Corporate CLA, click on "Please review the agreement" link, and then follow these steps
+These steps will confirm your organization association and you will only need to do these once. After completing these steps, the EasyCLA check will be complete and enabled for all future code contributions for this project.
+ +%s +%s`, + recipientName, projectName, addRemove, toFrom, + companyName, projectName, authUser.UserName, authorizedString, + utils.GetEmailHelpContent(claGroupModel.Version == utils.V2), utils.GetEmailSignOffContent()) + + err := utils.SendEmail(subject, body, recipients) + if err != nil { + logging.Warnf("problem sending email with subject: %s to recipients: %+v, error: %+v", subject, recipients, err) + } else { + logging.Debugf("sent email with subject: %s to recipients: %+v", subject, recipients) + } +} + +// getBestEmail is a helper function to return the best email address for the user model +func getBestEmail(userModel *models.User) string { + f := logrus.Fields{ + "functionName": "getBestEmail", + } + + if userModel != nil { + if userModel.LfEmail != "" { + return userModel.LfEmail.String() + } + + for _, email := range userModel.Emails { + if email != "" && !strings.Contains(email, "noreply.github.com") { + return email + } + } + } else { + logging.WithFields(f).Warn("user model is nil") + } + + return "" +} + +func (s service) sendRequestAccessEmailToContributors(authUser *auth.User, companyModel *models.Company, claGroupModel *models.ClaGroup, approvalList *models.ApprovalList) { + addEmailUsers := s.getAddEmailContributors(approvalList) + for _, user := range addEmailUsers { + sendRequestAccessEmailToContributorRecipient(authUser, companyModel, claGroupModel, user.Username, user.LfEmail.String(), "added", "to", + fmt.Sprintf("you are authorized to contribute to %s on behalf of %s", claGroupModel.ProjectName, companyModel.CompanyName)) + } + removeEmailUsers := s.getRemoveEmailContributors(approvalList) + for _, user := range removeEmailUsers { + sendRequestAccessEmailToContributorRecipient(authUser, companyModel, claGroupModel, user.Username, user.LfEmail.String(), "removed", "from", + fmt.Sprintf("you are no longer authorized to contribute to %s on behalf of %s ", claGroupModel.ProjectName, companyModel.CompanyName)) + } + addGitHubUsers := s.getAddGitHubContributors(approvalList) + for _, user := range addGitHubUsers { + sendRequestAccessEmailToContributorRecipient(authUser, companyModel, claGroupModel, user.Username, user.LfEmail.String(), "added", "to", + fmt.Sprintf("you are authorized to contribute to %s on behalf of %s", claGroupModel.ProjectName, companyModel.CompanyName)) + } + removeGitHubUsers := s.getRemoveGitHubContributors(approvalList) + for _, user := range removeGitHubUsers { + sendRequestAccessEmailToContributorRecipient(authUser, companyModel, claGroupModel, user.Username, user.LfEmail.String(), "removed", "from", + fmt.Sprintf("you are no longer authorized to contribute to %s on behalf of %s ", claGroupModel.ProjectName, companyModel.CompanyName)) + } + addGitlabUsers := s.getAddGitlabContributors(approvalList) + for _, user := range addGitlabUsers { + sendRequestAccessEmailToContributorRecipient(authUser, companyModel, claGroupModel, user.Username, user.LfEmail.String(), "added", "to", + fmt.Sprintf("you are authorized to contribute to %s on behalf of %s", claGroupModel.ProjectName, companyModel.CompanyName)) + } + removeGitlabUsers := s.getRemoveGitlabContributors(approvalList) + for _, user := range removeGitlabUsers { + sendRequestAccessEmailToContributorRecipient(authUser, companyModel, claGroupModel, user.Username, user.LfEmail.String(), "removed", "from", + fmt.Sprintf("you are no longer authorized to contribute to %s on behalf of %s ", claGroupModel.ProjectName, companyModel.CompanyName)) + } +} + +// sendRequestAccessEmailToCLAManagers sends the request access email to the specified CLA Managers +func (s service) sendApprovalListUpdateEmailToCLAManagers(companyModel *models.Company, claGroupModel *models.ClaGroup, recipientName, recipientAddress string, approvalListChanges *models.ApprovalList) { + f := logrus.Fields{ + "functionName": "sendApprovalListUpdateEmailToCLAManagers", + "projectName": claGroupModel.ProjectName, + "projectExternalID": claGroupModel.ProjectExternalID, + "foundationSFID": claGroupModel.FoundationSFID, + "companyName": companyModel.CompanyName, + "companyExternalID": companyModel.CompanyExternalID, + "recipientName": recipientName, + "recipientAddress": recipientAddress} + + companyName := companyModel.CompanyName + projectName := claGroupModel.ProjectName + + // subject string, body string, recipients []string + subject := fmt.Sprintf("EasyCLA: Approval List Update for %s on %s", companyName, projectName) + recipients := []string{recipientAddress} + body := fmt.Sprintf(` +Hello %s,
+This is a notification email from EasyCLA regarding the project %s.
+The EasyCLA approval list for %s for project %s was modified.
+The modification was as follows:
+%s +%s +%s`, + recipientName, projectName, companyName, projectName, buildApprovalListSummary(approvalListChanges), + utils.GetEmailHelpContent(claGroupModel.Version == utils.V2), utils.GetEmailSignOffContent()) + + err := utils.SendEmail(subject, body, recipients) + if err != nil { + logging.WithFields(f).Warnf("problem sending email with subject: %s to recipients: %+v, error: %+v", subject, recipients, err) + } else { + logging.WithFields(f).Debugf("sent email with subject: %s to recipients: %+v", subject, recipients) + } +} diff --git a/cla-backend-go/signatures/events.go b/cla-backend-go/signatures/events.go new file mode 100644 index 000000000..60f7ddfba --- /dev/null +++ b/cla-backend-go/signatures/events.go @@ -0,0 +1,220 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package signatures + +import ( + "context" + + "github.com/communitybridge/easycla/cla-backend-go/events" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" +) + +func (s service) createEventLogEntries(ctx context.Context, companyModel *models.Company, claGroupModel *models.ClaGroup, userModel *models.User, approvalList *models.ApprovalList, projectSFID string) { + for _, value := range approvalList.AddEmailApprovalList { + // Send an event + s.eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ClaApprovalListUpdated, + ProjectID: claGroupModel.ProjectExternalID, + ClaGroupModel: claGroupModel, + CompanyID: companyModel.CompanyID, + CompanyModel: companyModel, + LfUsername: userModel.LfUsername, + UserID: userModel.UserID, + UserModel: userModel, + ProjectSFID: projectSFID, + EventData: &events.CLAApprovalListAddEmailData{ + ApprovalListEmail: value, + }, + }) + } + for _, value := range approvalList.RemoveEmailApprovalList { + // Send an event + s.eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ClaApprovalListUpdated, + ProjectID: claGroupModel.ProjectExternalID, + ClaGroupModel: claGroupModel, + CompanyID: companyModel.CompanyID, + CompanyModel: companyModel, + LfUsername: userModel.LfUsername, + UserID: userModel.UserID, + UserModel: userModel, + ProjectSFID: projectSFID, + EventData: &events.CLAApprovalListRemoveEmailData{ + ApprovalListEmail: value, + }, + }) + } + for _, value := range approvalList.AddDomainApprovalList { + // Send an event + s.eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ClaApprovalListUpdated, + ProjectID: claGroupModel.ProjectExternalID, + ClaGroupModel: claGroupModel, + CompanyID: companyModel.CompanyID, + CompanyModel: companyModel, + LfUsername: userModel.LfUsername, + UserID: userModel.UserID, + UserModel: userModel, + ProjectSFID: projectSFID, + EventData: &events.CLAApprovalListAddDomainData{ + ApprovalListDomain: value, + }, + }) + } + for _, value := range approvalList.RemoveDomainApprovalList { + // Send an event + s.eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ClaApprovalListUpdated, + ProjectID: claGroupModel.ProjectExternalID, + ClaGroupModel: claGroupModel, + CompanyID: companyModel.CompanyID, + CompanyModel: companyModel, + LfUsername: userModel.LfUsername, + UserID: userModel.UserID, + UserModel: userModel, + ProjectSFID: projectSFID, + EventData: &events.CLAApprovalListRemoveDomainData{ + ApprovalListDomain: value, + }, + }) + } + for _, value := range approvalList.AddGithubUsernameApprovalList { + // Send an event + s.eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ClaApprovalListUpdated, + ProjectID: claGroupModel.ProjectExternalID, + ClaGroupModel: claGroupModel, + CompanyID: companyModel.CompanyID, + CompanyModel: companyModel, + LfUsername: userModel.LfUsername, + UserID: userModel.UserID, + UserModel: userModel, + ProjectSFID: projectSFID, + EventData: &events.CLAApprovalListAddGitHubUsernameData{ + ApprovalListGitHubUsername: value, + }, + }) + } + for _, value := range approvalList.RemoveGithubUsernameApprovalList { + // Send an event + s.eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ClaApprovalListUpdated, + ProjectID: claGroupModel.ProjectExternalID, + ClaGroupModel: claGroupModel, + CompanyID: companyModel.CompanyID, + CompanyModel: companyModel, + LfUsername: userModel.LfUsername, + UserID: userModel.UserID, + UserModel: userModel, + ProjectSFID: projectSFID, + EventData: &events.CLAApprovalListRemoveGitHubUsernameData{ + ApprovalListGitHubUsername: value, + }, + }) + } + for _, value := range approvalList.AddGithubOrgApprovalList { + // Send an event + s.eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ClaApprovalListUpdated, + ProjectID: claGroupModel.ProjectExternalID, + ClaGroupModel: claGroupModel, + CompanyID: companyModel.CompanyID, + CompanyModel: companyModel, + LfUsername: userModel.LfUsername, + UserID: userModel.UserID, + UserModel: userModel, + ProjectSFID: projectSFID, + EventData: &events.CLAApprovalListAddGitHubOrgData{ + ApprovalListGitHubOrg: value, + }, + }) + } + for _, value := range approvalList.RemoveGithubOrgApprovalList { + // Send an event + s.eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ClaApprovalListUpdated, + CLAGroupID: claGroupModel.ProjectID, + ProjectID: claGroupModel.ProjectExternalID, + ClaGroupModel: claGroupModel, + CompanyID: companyModel.CompanyID, + CompanyModel: companyModel, + LfUsername: userModel.LfUsername, + UserID: userModel.UserID, + UserModel: userModel, + ProjectSFID: projectSFID, + EventData: &events.CLAApprovalListRemoveGitHubOrgData{ + ApprovalListGitHubOrg: value, + }, + }) + } + for _, value := range approvalList.AddGitlabUsernameApprovalList { + // Send an event + s.eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ClaApprovalListUpdated, + ProjectID: claGroupModel.ProjectExternalID, + ClaGroupModel: claGroupModel, + CompanyID: companyModel.CompanyID, + CompanyModel: companyModel, + LfUsername: userModel.LfUsername, + UserID: userModel.UserID, + UserModel: userModel, + ProjectSFID: projectSFID, + EventData: &events.CLAApprovalListAddGitLabUsernameData{ + ApprovalListGitLabUsername: value, + }, + }) + } + for _, value := range approvalList.RemoveGitlabUsernameApprovalList { + // Send an event + s.eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ClaApprovalListUpdated, + ProjectID: claGroupModel.ProjectExternalID, + ClaGroupModel: claGroupModel, + CompanyID: companyModel.CompanyID, + CompanyModel: companyModel, + LfUsername: userModel.LfUsername, + UserID: userModel.UserID, + UserModel: userModel, + ProjectSFID: projectSFID, + EventData: &events.CLAApprovalListRemoveGitLabUsernameData{ + ApprovalListGitLabUsername: value, + }, + }) + } + for _, value := range approvalList.AddGitlabOrgApprovalList { + // Send an event + s.eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ClaApprovalListUpdated, + ProjectID: claGroupModel.ProjectExternalID, + ClaGroupModel: claGroupModel, + CompanyID: companyModel.CompanyID, + CompanyModel: companyModel, + LfUsername: userModel.LfUsername, + UserID: userModel.UserID, + UserModel: userModel, + ProjectSFID: projectSFID, + EventData: &events.CLAApprovalListAddGitLabGroupData{ + ApprovalListGitLabGroup: value, + }, + }) + } + for _, value := range approvalList.RemoveGitlabOrgApprovalList { + // Send an event + s.eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ClaApprovalListUpdated, + CLAGroupID: claGroupModel.ProjectID, + ProjectID: claGroupModel.ProjectExternalID, + ClaGroupModel: claGroupModel, + CompanyID: companyModel.CompanyID, + CompanyModel: companyModel, + LfUsername: userModel.LfUsername, + UserID: userModel.UserID, + UserModel: userModel, + ProjectSFID: projectSFID, + EventData: &events.CLAApprovalListRemoveGitLabGroupData{ + ApprovalListGitLabGroup: value, + }, + }) + } +} diff --git a/cla-backend-go/signatures/handlers.go b/cla-backend-go/signatures/handlers.go index 6cc246320..5b4c02406 100644 --- a/cla-backend-go/signatures/handlers.go +++ b/cla-backend-go/signatures/handlers.go @@ -8,10 +8,12 @@ import ( "fmt" "net/http" + "github.com/sirupsen/logrus" + "github.com/communitybridge/easycla/cla-backend-go/events" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/signatures" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/signatures" "github.com/communitybridge/easycla/cla-backend-go/github" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/user" @@ -27,25 +29,31 @@ func Configure(api *operations.ClaAPI, service SignatureService, sessionStore *d api.SignaturesGetSignedICLADocumentHandler = signatures.GetSignedICLADocumentHandlerFunc(func(params signatures.GetSignedICLADocumentParams) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint - signatureModel, sigErr := service.GetIndividualSignature(ctx, params.ClaGroupID, params.UserID) + + f := logrus.Fields{ + "functionName": "v1.signatures.handler.SignaturesGetSignedICLADocumentHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": params.ClaGroupID, + "userID": params.UserID, + } + + log.WithFields(f).Debug("querying for individual signature...") + approved, signed := true, true + signatureModel, sigErr := service.GetIndividualSignature(ctx, params.ClaGroupID, params.UserID, &approved, &signed) if sigErr != nil { - msg := fmt.Sprintf("EasyCLA - 500 Internal Server Error - error retrieving signature using ClaGroupID: %s, userID: %s, error: %+v", + msg := fmt.Sprintf("error retrieving signature using ClaGroupID: %s, userID: %s, error: %+v", params.ClaGroupID, params.UserID, sigErr) - log.Warn(msg) - return signatures.NewGetSignedICLADocumentInternalServerError().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "500", - Message: msg, - }) + log.WithFields(f).WithError(sigErr).Warn(msg) + return signatures.NewGetSignedICLADocumentInternalServerError().WithXRequestID(reqID).WithPayload( + utils.ToV1ErrorResponse(utils.ErrorResponseInternalServerErrorWithError(reqID, msg, sigErr))) } if signatureModel == nil { - msg := fmt.Sprintf("EasyCLA - 404 Not Found - - error retrieving signature using claGroupID: %s, userID: %s", + msg := fmt.Sprintf("error retrieving signature using claGroupID: %s, userID: %s", params.ClaGroupID, params.UserID) - log.Warn(msg) - return signatures.NewGetSignedICLADocumentNotFound().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "404", - Message: msg, - }) + log.WithFields(f).Warn(msg) + return signatures.NewGetSignedICLADocumentNotFound().WithXRequestID(reqID).WithPayload( + utils.ToV1ErrorResponse(utils.ErrorResponseNotFound(reqID, msg))) } downloadURL := fmt.Sprintf("contract-group/%s/icla/%s/%s.pdf", @@ -53,13 +61,11 @@ func Configure(api *operations.ClaAPI, service SignatureService, sessionStore *d log.Debugf("Retrieving PDF from path: %s", downloadURL) downloadLink, s3Err := utils.GetDownloadLink(downloadURL) if s3Err != nil { - msg := fmt.Sprintf("EasyCLA - 500 Internal Server Error - unable to locate PDF from source using ClaGroupID: %s, userID: %s, s3 error: %+v", + msg := fmt.Sprintf("unable to locate PDF from source using ClaGroupID: %s, userID: %s, s3 error: %+v", params.ClaGroupID, params.UserID, s3Err) log.Warn(msg) - return signatures.NewGetSignedICLADocumentInternalServerError().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "500", - Message: msg, - }) + return signatures.NewGetSignedICLADocumentInternalServerError().WithXRequestID(reqID).WithPayload( + utils.ToV1ErrorResponse(utils.ErrorResponseInternalServerErrorWithError(reqID, msg, sigErr))) } return middleware.ResponderFunc(func(rw http.ResponseWriter, p runtime.Producer) { @@ -80,22 +86,29 @@ func Configure(api *operations.ClaAPI, service SignatureService, sessionStore *d api.SignaturesGetSignedCCLADocumentHandler = signatures.GetSignedCCLADocumentHandlerFunc(func(params signatures.GetSignedCCLADocumentParams) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint - signatureModel, sigErr := service.GetCorporateSignature(ctx, params.ClaGroupID, params.CompanyID) + f := logrus.Fields{ + "functionName": "v1.signatures.handler.SignaturesGetSignedCCLADocumentHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": params.ClaGroupID, + "companyID": params.CompanyID, + } + + approved, signed := true, true + signatureModel, sigErr := service.GetCorporateSignature(ctx, params.ClaGroupID, params.CompanyID, &approved, &signed) if sigErr != nil { msg := fmt.Sprintf("EasyCLA - 500 Internal Server Error - error retrieving signature using ClaGroupID: %s, CompanyID: %s, error: %+v", params.ClaGroupID, params.CompanyID, sigErr) - log.Warn(msg) + log.WithFields(f).WithError(sigErr).Warn(msg) return signatures.NewGetSignedCCLADocumentInternalServerError().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Code: "500", Message: msg, }) - } if signatureModel == nil { msg := fmt.Sprintf("EasyCLA - 404 Not Found - - error retrieving signature using ClaGroupID: %s, CompanyID: %s", params.ClaGroupID, params.CompanyID) - log.Warn(msg) + log.WithFields(f).Warn(msg) return signatures.NewGetSignedCCLADocumentNotFound().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Code: "404", Message: msg, @@ -109,7 +122,7 @@ func Configure(api *operations.ClaAPI, service SignatureService, sessionStore *d if s3Err != nil { msg := fmt.Sprintf("EasyCLA - 500 Internal Server Error - unable to locate PDF from source using ClaGroupID: %s, CompanyID: %s, s3 error: %+v", params.ClaGroupID, params.CompanyID, s3Err) - log.Warn(msg) + log.WithFields(f).WithError(s3Err).Warn(msg) return signatures.NewGetSignedCCLADocumentInternalServerError().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Code: "500", Message: msg, @@ -125,9 +138,9 @@ func Configure(api *operations.ClaAPI, service SignatureService, sessionStore *d if writeErr != nil { msg := fmt.Sprintf("EasyCLA - 500 Internal Server Error - generating s3 redirect for the client client using source using ClaGroupID: %s, CompanyID: %s, error: %+v", params.ClaGroupID, params.CompanyID, s3Err) - log.Warn(msg) + log.WithFields(f).WithError(writeErr).Warn(msg) } - log.Debugf("SignaturesGetSignedICLADocumentHandler - wrote %d bytes", bytesWritten) + log.WithFields(f).Debugf("wrote %d bytes", bytesWritten) }) }) @@ -164,7 +177,7 @@ func Configure(api *operations.ClaAPI, service SignatureService, sessionStore *d githubAccessToken = "" } - ghApprovalList, err := service.GetGithubOrganizationsFromWhitelist(ctx, params.SignatureID, githubAccessToken) + ghApprovalList, err := service.GetGithubOrganizationsFromApprovalList(ctx, params.SignatureID, githubAccessToken) if err != nil { log.Warnf("error fetching github organization approval list entries v using signature_id: %s, error: %+v", params.SignatureID, err) @@ -190,7 +203,7 @@ func Configure(api *operations.ClaAPI, service SignatureService, sessionStore *d githubAccessToken = "" } - ghApprovalList, err := service.AddGithubOrganizationToWhitelist(ctx, params.SignatureID, params.Body, githubAccessToken) + ghApprovalList, err := service.AddGithubOrganizationToApprovalList(ctx, params.SignatureID, params.Body, githubAccessToken) if err != nil { log.Warnf("error adding github organization %s using signature_id: %s to the whitelist, error: %+v", *params.Body.OrganizationID, params.SignatureID, err) @@ -207,9 +220,9 @@ func Configure(api *operations.ClaAPI, service SignatureService, sessionStore *d } if signatureModel != nil { projectID = signatureModel.ProjectID - companyID = signatureModel.SignatureReferenceID.String() + companyID = signatureModel.SignatureReferenceID } - eventsService.LogEvent(&events.LogEventArgs{ + eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.ApprovalListGitHubOrganizationAdded, ProjectID: projectID, CompanyID: companyID, @@ -240,7 +253,7 @@ func Configure(api *operations.ClaAPI, service SignatureService, sessionStore *d githubAccessToken = "" } - ghApprovalList, err := service.DeleteGithubOrganizationFromWhitelist(ctx, params.SignatureID, params.Body, githubAccessToken) + ghApprovalList, err := service.DeleteGithubOrganizationFromApprovalList(ctx, params.SignatureID, params.Body, githubAccessToken) if err != nil { log.Warnf("error deleting github organization %s using signature_id: %s from the whitelist, error: %+v", *params.Body.OrganizationID, params.SignatureID, err) @@ -257,10 +270,10 @@ func Configure(api *operations.ClaAPI, service SignatureService, sessionStore *d } if signatureModel != nil { projectID = signatureModel.ProjectID - companyID = signatureModel.SignatureReferenceID.String() + companyID = signatureModel.SignatureReferenceID } - eventsService.LogEvent(&events.LogEventArgs{ + eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.ApprovalListGitHubOrganizationDeleted, ProjectID: projectID, CompanyID: companyID, @@ -288,6 +301,32 @@ func Configure(api *operations.ClaAPI, service SignatureService, sessionStore *d return signatures.NewGetProjectSignaturesOK().WithXRequestID(reqID).WithPayload(projectSignatures) }) + api.SignaturesCreateProjectSummaryReportHandler = signatures.CreateProjectSummaryReportHandlerFunc(func(params signatures.CreateProjectSummaryReportParams, claUser *user.CLAUser) middleware.Responder { + reqID := utils.GetRequestID(params.XREQUESTID) + ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + f := logrus.Fields{ + "functionName": "signature.handlers.SignaturesCreateProjectSummaryReportHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectID": params.ProjectID, + "claType": utils.StringValue(params.ClaType), + "signatureType": utils.StringValue(params.SignatureType), + "nextKey": utils.StringValue(params.NextKey), + "searchField": utils.StringValue(params.SearchField), + "searchTerm": utils.StringValue(params.SearchTerm), + "sortOrder": utils.StringValue(params.SortOrder), + "fullMatch": utils.BoolValue(params.FullMatch), + "pageSize": utils.Int64Value(params.PageSize), + } + projectSummaryReport, err := service.CreateProjectSummaryReport(ctx, params) + if err != nil { + log.WithFields(f).WithError(err).Warnf("error creating project summary report for projectID: %s, error: %+v", + params.ProjectID, err) + return signatures.NewGetProjectSignaturesBadRequest().WithPayload(errorResponse(err)) + } + + return signatures.NewCreateProjectSummaryReportOK().WithXRequestID(reqID).WithPayload(projectSummaryReport) + }) + // Get Project Company Signatures api.SignaturesGetProjectCompanySignaturesHandler = signatures.GetProjectCompanySignaturesHandlerFunc(func(params signatures.GetProjectCompanySignaturesParams) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) @@ -318,7 +357,7 @@ func Configure(api *operations.ClaAPI, service SignatureService, sessionStore *d api.SignaturesGetProjectCompanyEmployeeSignaturesHandler = signatures.GetProjectCompanyEmployeeSignaturesHandlerFunc(func(params signatures.GetProjectCompanyEmployeeSignaturesParams, claUser *user.CLAUser) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint - projectSignatures, err := service.GetProjectCompanyEmployeeSignatures(ctx, params) + projectSignatures, err := service.GetProjectCompanyEmployeeSignatures(ctx, params, nil) if err != nil { log.Warnf("error retrieving employee project signatures for project: %s, company: %s, error: %+v", params.ProjectID, params.CompanyID, err) @@ -345,7 +384,7 @@ func Configure(api *operations.ClaAPI, service SignatureService, sessionStore *d api.SignaturesGetUserSignaturesHandler = signatures.GetUserSignaturesHandlerFunc(func(params signatures.GetUserSignaturesParams, claUser *user.CLAUser) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint - userSignatures, err := service.GetUserSignatures(ctx, params) + userSignatures, err := service.GetUserSignatures(ctx, params, nil) if err != nil { log.Warnf("error retrieving user signatures for userID: %s, error: %+v", params.UserID, err) return signatures.NewGetUserSignaturesBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(err)) diff --git a/cla-backend-go/signatures/helpers.go b/cla-backend-go/signatures/helpers.go new file mode 100644 index 000000000..6507567f2 --- /dev/null +++ b/cla-backend-go/signatures/helpers.go @@ -0,0 +1,132 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package signatures + +import ( + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/user" + "github.com/go-openapi/strfmt" +) + +// getAddEmailContributors is a helper function to lookup the contributors impacted by the Approval List update +func (s service) getAddEmailContributors(approvalList *models.ApprovalList) []*models.User { + var userModelList []*models.User + for _, value := range approvalList.AddEmailApprovalList { + userModel, err := s.usersService.GetUserByEmail(value) + if err != nil { + logging.Warnf("unable to lookup user by LF email: %s, error: %+v", value, err) + } else { + userModelList = append(userModelList, userModel) + } + } + + return userModelList +} + +// getRemoveEmailContributors is a helper function to lookup the contributors impacted by the Approval List update +func (s service) getRemoveEmailContributors(approvalList *models.ApprovalList) []*models.User { + var userModelList []*models.User + for _, value := range approvalList.RemoveEmailApprovalList { + userModel, err := s.usersService.GetUserByEmail(value) + if err != nil { + logging.Warnf("unable to lookup user by LF email: %s, error: %+v", value, err) + } else { + userModelList = append(userModelList, userModel) + } + } + + return userModelList +} + +// getAddGitHubContributors is a helper function to lookup the contributors impacted by the Approval List update +func (s service) getAddGitHubContributors(approvalList *models.ApprovalList) []*models.User { + var userModelList []*models.User + for _, value := range approvalList.AddGithubUsernameApprovalList { + userModel, err := s.usersService.GetUserByGitHubUsername(value) + if err != nil { + logging.Warnf("unable to lookup user by GitHub username: %s, error: %+v", value, err) + } else { + userModelList = append(userModelList, userModel) + } + } + + return userModelList +} + +// getRemoveGitHubContributors is a helper function to lookup the contributors impacted by the Approval List update +func (s service) getRemoveGitHubContributors(approvalList *models.ApprovalList) []*models.User { + var userModelList []*models.User + for _, value := range approvalList.RemoveGithubUsernameApprovalList { + userModel, err := s.usersService.GetUserByGitHubUsername(value) + if err != nil { + logging.Warnf("unable to lookup user by GitHub username: %s, error: %+v", value, err) + } else { + userModelList = append(userModelList, userModel) + } + } + + return userModelList +} + +// getAddGitlabContributors is a helper function to look up the Gitlab contributors impacted by the Approval List update +func (s service) getAddGitlabContributors(approvalList *models.ApprovalList) []*models.User { + var userModelList []*models.User + for _, value := range approvalList.AddGitlabUsernameApprovalList { + userModel, err := s.usersService.GetUserByGitHubUsername(value) + if err != nil { + logging.Warnf("unable to lookup user by Gitlab username: %s, error: %+v", value, err) + } else { + userModelList = append(userModelList, userModel) + } + } + + return userModelList +} + +// getRemoveGitlabContributors is a helper function to look up the Gitlab contributors impacted by the Approval List update +func (s service) getRemoveGitlabContributors(approvalList *models.ApprovalList) []*models.User { + var userModelList []*models.User + for _, value := range approvalList.RemoveGitlabUsernameApprovalList { + userModel, err := s.usersService.GetUserByGitHubUsername(value) + if err != nil { + logging.Warnf("unable to lookup user by Gitlab username: %s, error: %+v", value, err) + } else { + userModelList = append(userModelList, userModel) + } + } + + return userModelList +} + +func (s service) createUserModel(gitHubUsername, gitHubUserID, gitLabUsername, gitLabUserID, email, companyID, note string) (*models.User, error) { + userModel := models.User{ + Admin: false, + CompanyID: companyID, + Note: note, + } + // Email + if email != "" { + userModel.LfEmail = strfmt.Email(email) + userModel.Emails = []string{email} + } + + // GitHub info + if gitHubUserID != "" { + userModel.GithubID = gitHubUserID + } + if gitHubUsername != "" { + userModel.GithubUsername = gitHubUsername + } + + // GitLab info + if gitLabUserID != "" { + userModel.GitlabID = gitLabUserID + } + if gitLabUsername != "" { + userModel.GitlabUsername = gitLabUsername + } + + return s.usersService.CreateUser(&userModel, &user.CLAUser{}) +} diff --git a/cla-backend-go/signatures/mocks/mock_repo.go b/cla-backend-go/signatures/mocks/mock_repo.go new file mode 100644 index 000000000..8ebe7653d --- /dev/null +++ b/cla-backend-go/signatures/mocks/mock_repo.go @@ -0,0 +1,629 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +// Code generated by MockGen. DO NOT EDIT. +// Source: signatures/repository.go + +// Package mock_signatures is a generated GoMock package. +package mock_signatures + +import ( + context "context" + reflect "reflect" + sync "sync" + + events "github.com/communitybridge/easycla/cla-backend-go/events" + models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + signatures "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/signatures" + signatures0 "github.com/communitybridge/easycla/cla-backend-go/signatures" + gomock "github.com/golang/mock/gomock" +) + +// MockSignatureRepository is a mock of SignatureRepository interface. +type MockSignatureRepository struct { + ctrl *gomock.Controller + recorder *MockSignatureRepositoryMockRecorder +} + +// MockSignatureRepositoryMockRecorder is the mock recorder for MockSignatureRepository. +type MockSignatureRepositoryMockRecorder struct { + mock *MockSignatureRepository +} + +// NewMockSignatureRepository creates a new mock instance. +func NewMockSignatureRepository(ctrl *gomock.Controller) *MockSignatureRepository { + mock := &MockSignatureRepository{ctrl: ctrl} + mock.recorder = &MockSignatureRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSignatureRepository) EXPECT() *MockSignatureRepositoryMockRecorder { + return m.recorder +} + +// ActivateSignature mocks base method. +func (m *MockSignatureRepository) ActivateSignature(ctx context.Context, signatureID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ActivateSignature", ctx, signatureID) + ret0, _ := ret[0].(error) + return ret0 +} + +// ActivateSignature indicates an expected call of ActivateSignature. +func (mr *MockSignatureRepositoryMockRecorder) ActivateSignature(ctx, signatureID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActivateSignature", reflect.TypeOf((*MockSignatureRepository)(nil).ActivateSignature), ctx, signatureID) +} + +// AddCLAManager mocks base method. +func (m *MockSignatureRepository) AddCLAManager(ctx context.Context, signatureID, claManagerID string) (*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddCLAManager", ctx, signatureID, claManagerID) + ret0, _ := ret[0].(*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddCLAManager indicates an expected call of AddCLAManager. +func (mr *MockSignatureRepositoryMockRecorder) AddCLAManager(ctx, signatureID, claManagerID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddCLAManager", reflect.TypeOf((*MockSignatureRepository)(nil).AddCLAManager), ctx, signatureID, claManagerID) +} + +// AddGithubOrganizationToApprovalList mocks base method. +func (m *MockSignatureRepository) AddGithubOrganizationToApprovalList(ctx context.Context, signatureID, githubOrganizationID string) ([]models.GithubOrg, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddGithubOrganizationToApprovalList", ctx, signatureID, githubOrganizationID) + ret0, _ := ret[0].([]models.GithubOrg) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddGithubOrganizationToApprovalList indicates an expected call of AddGithubOrganizationToApprovalList. +func (mr *MockSignatureRepositoryMockRecorder) AddGithubOrganizationToApprovalList(ctx, signatureID, githubOrganizationID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGithubOrganizationToApprovalList", reflect.TypeOf((*MockSignatureRepository)(nil).AddGithubOrganizationToApprovalList), ctx, signatureID, githubOrganizationID) +} + +// AddSigTypeSignedApprovedID mocks base method. +func (m *MockSignatureRepository) AddSigTypeSignedApprovedID(ctx context.Context, signatureID, val string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddSigTypeSignedApprovedID", ctx, signatureID, val) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddSigTypeSignedApprovedID indicates an expected call of AddSigTypeSignedApprovedID. +func (mr *MockSignatureRepositoryMockRecorder) AddSigTypeSignedApprovedID(ctx, signatureID, val interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddSigTypeSignedApprovedID", reflect.TypeOf((*MockSignatureRepository)(nil).AddSigTypeSignedApprovedID), ctx, signatureID, val) +} + +// AddSignedOn mocks base method. +func (m *MockSignatureRepository) AddSignedOn(ctx context.Context, signatureID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddSignedOn", ctx, signatureID) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddSignedOn indicates an expected call of AddSignedOn. +func (mr *MockSignatureRepositoryMockRecorder) AddSignedOn(ctx, signatureID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddSignedOn", reflect.TypeOf((*MockSignatureRepository)(nil).AddSignedOn), ctx, signatureID) +} + +// AddUsersDetails mocks base method. +func (m *MockSignatureRepository) AddUsersDetails(ctx context.Context, signatureID, userID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddUsersDetails", ctx, signatureID, userID) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddUsersDetails indicates an expected call of AddUsersDetails. +func (mr *MockSignatureRepositoryMockRecorder) AddUsersDetails(ctx, signatureID, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUsersDetails", reflect.TypeOf((*MockSignatureRepository)(nil).AddUsersDetails), ctx, signatureID, userID) +} + +// CreateProjectCompanyEmployeeSignature mocks base method. +func (m *MockSignatureRepository) CreateProjectCompanyEmployeeSignature(ctx context.Context, companyModel *models.Company, claGroupModel *models.ClaGroup, employeeUserModel *models.User) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateProjectCompanyEmployeeSignature", ctx, companyModel, claGroupModel, employeeUserModel) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateProjectCompanyEmployeeSignature indicates an expected call of CreateProjectCompanyEmployeeSignature. +func (mr *MockSignatureRepositoryMockRecorder) CreateProjectCompanyEmployeeSignature(ctx, companyModel, claGroupModel, employeeUserModel interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateProjectCompanyEmployeeSignature", reflect.TypeOf((*MockSignatureRepository)(nil).CreateProjectCompanyEmployeeSignature), ctx, companyModel, claGroupModel, employeeUserModel) +} + +// CreateProjectSummaryReport mocks base method. +func (m *MockSignatureRepository) CreateProjectSummaryReport(ctx context.Context, params signatures.CreateProjectSummaryReportParams) (*models.SignatureReport, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateProjectSummaryReport", ctx, params) + ret0, _ := ret[0].(*models.SignatureReport) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateProjectSummaryReport indicates an expected call of CreateProjectSummaryReport. +func (mr *MockSignatureRepositoryMockRecorder) CreateProjectSummaryReport(ctx, params interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateProjectSummaryReport", reflect.TypeOf((*MockSignatureRepository)(nil).CreateProjectSummaryReport), ctx, params) +} + +// CreateSignature mocks base method. +func (m *MockSignatureRepository) CreateSignature(ctx context.Context, signature *signatures0.ItemSignature) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateSignature", ctx, signature) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateSignature indicates an expected call of CreateSignature. +func (mr *MockSignatureRepositoryMockRecorder) CreateSignature(ctx, signature interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSignature", reflect.TypeOf((*MockSignatureRepository)(nil).CreateSignature), ctx, signature) +} + +// DeleteGithubOrganizationFromApprovalList mocks base method. +func (m *MockSignatureRepository) DeleteGithubOrganizationFromApprovalList(ctx context.Context, signatureID, githubOrganizationID string) ([]models.GithubOrg, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteGithubOrganizationFromApprovalList", ctx, signatureID, githubOrganizationID) + ret0, _ := ret[0].([]models.GithubOrg) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteGithubOrganizationFromApprovalList indicates an expected call of DeleteGithubOrganizationFromApprovalList. +func (mr *MockSignatureRepositoryMockRecorder) DeleteGithubOrganizationFromApprovalList(ctx, signatureID, githubOrganizationID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteGithubOrganizationFromApprovalList", reflect.TypeOf((*MockSignatureRepository)(nil).DeleteGithubOrganizationFromApprovalList), ctx, signatureID, githubOrganizationID) +} + +// EclaAutoCreate mocks base method. +func (m *MockSignatureRepository) EclaAutoCreate(ctx context.Context, signatureID string, autoCreateECLA bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EclaAutoCreate", ctx, signatureID, autoCreateECLA) + ret0, _ := ret[0].(error) + return ret0 +} + +// EclaAutoCreate indicates an expected call of EclaAutoCreate. +func (mr *MockSignatureRepositoryMockRecorder) EclaAutoCreate(ctx, signatureID, autoCreateECLA interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EclaAutoCreate", reflect.TypeOf((*MockSignatureRepository)(nil).EclaAutoCreate), ctx, signatureID, autoCreateECLA) +} + +// GetActivePullRequestMetadata mocks base method. +func (m *MockSignatureRepository) GetActivePullRequestMetadata(ctx context.Context, gitHubAuthorUsername, gitHubAuthorEmail string) (*signatures0.ActivePullRequest, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetActivePullRequestMetadata", ctx, gitHubAuthorUsername, gitHubAuthorEmail) + ret0, _ := ret[0].(*signatures0.ActivePullRequest) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetActivePullRequestMetadata indicates an expected call of GetActivePullRequestMetadata. +func (mr *MockSignatureRepositoryMockRecorder) GetActivePullRequestMetadata(ctx, gitHubAuthorUsername, gitHubAuthorEmail interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActivePullRequestMetadata", reflect.TypeOf((*MockSignatureRepository)(nil).GetActivePullRequestMetadata), ctx, gitHubAuthorUsername, gitHubAuthorEmail) +} + +// GetCCLASignatures mocks base method. +func (m *MockSignatureRepository) GetCCLASignatures(ctx context.Context, signed, approved *bool) ([]*signatures0.ItemSignature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCCLASignatures", ctx, signed, approved) + ret0, _ := ret[0].([]*signatures0.ItemSignature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCCLASignatures indicates an expected call of GetCCLASignatures. +func (mr *MockSignatureRepositoryMockRecorder) GetCCLASignatures(ctx, signed, approved interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCCLASignatures", reflect.TypeOf((*MockSignatureRepository)(nil).GetCCLASignatures), ctx, signed, approved) +} + +// GetClaGroupCorporateContributors mocks base method. +func (m *MockSignatureRepository) GetClaGroupCorporateContributors(ctx context.Context, claGroupID string, companyID *string, pageSize *int64, nextKey, searchTerm *string) (*models.CorporateContributorList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClaGroupCorporateContributors", ctx, claGroupID, companyID, pageSize, nextKey, searchTerm) + ret0, _ := ret[0].(*models.CorporateContributorList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClaGroupCorporateContributors indicates an expected call of GetClaGroupCorporateContributors. +func (mr *MockSignatureRepositoryMockRecorder) GetClaGroupCorporateContributors(ctx, claGroupID, companyID, pageSize, nextKey, searchTerm interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaGroupCorporateContributors", reflect.TypeOf((*MockSignatureRepository)(nil).GetClaGroupCorporateContributors), ctx, claGroupID, companyID, pageSize, nextKey, searchTerm) +} + +// GetClaGroupICLASignatures mocks base method. +func (m *MockSignatureRepository) GetClaGroupICLASignatures(ctx context.Context, claGroupID string, searchTerm *string, approved, signed *bool, pageSize int64, nextKey string, withExtraDetails bool) (*models.IclaSignatures, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClaGroupICLASignatures", ctx, claGroupID, searchTerm, approved, signed, pageSize, nextKey, withExtraDetails) + ret0, _ := ret[0].(*models.IclaSignatures) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClaGroupICLASignatures indicates an expected call of GetClaGroupICLASignatures. +func (mr *MockSignatureRepositoryMockRecorder) GetClaGroupICLASignatures(ctx, claGroupID, searchTerm, approved, signed, pageSize, nextKey, withExtraDetails interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaGroupICLASignatures", reflect.TypeOf((*MockSignatureRepository)(nil).GetClaGroupICLASignatures), ctx, claGroupID, searchTerm, approved, signed, pageSize, nextKey, withExtraDetails) +} + +// GetCompanyIDsWithSignedCorporateSignatures mocks base method. +func (m *MockSignatureRepository) GetCompanyIDsWithSignedCorporateSignatures(ctx context.Context, claGroupID string) ([]signatures0.SignatureCompanyID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanyIDsWithSignedCorporateSignatures", ctx, claGroupID) + ret0, _ := ret[0].([]signatures0.SignatureCompanyID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanyIDsWithSignedCorporateSignatures indicates an expected call of GetCompanyIDsWithSignedCorporateSignatures. +func (mr *MockSignatureRepositoryMockRecorder) GetCompanyIDsWithSignedCorporateSignatures(ctx, claGroupID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanyIDsWithSignedCorporateSignatures", reflect.TypeOf((*MockSignatureRepository)(nil).GetCompanyIDsWithSignedCorporateSignatures), ctx, claGroupID) +} + +// GetCompanySignatures mocks base method. +func (m *MockSignatureRepository) GetCompanySignatures(ctx context.Context, params signatures.GetCompanySignaturesParams, pageSize int64, loadACL bool) (*models.Signatures, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanySignatures", ctx, params, pageSize, loadACL) + ret0, _ := ret[0].(*models.Signatures) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanySignatures indicates an expected call of GetCompanySignatures. +func (mr *MockSignatureRepositoryMockRecorder) GetCompanySignatures(ctx, params, pageSize, loadACL interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanySignatures", reflect.TypeOf((*MockSignatureRepository)(nil).GetCompanySignatures), ctx, params, pageSize, loadACL) +} + +// GetCorporateSignature mocks base method. +func (m *MockSignatureRepository) GetCorporateSignature(ctx context.Context, claGroupID, companyID string, approved, signed *bool) (*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCorporateSignature", ctx, claGroupID, companyID, approved, signed) + ret0, _ := ret[0].(*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCorporateSignature indicates an expected call of GetCorporateSignature. +func (mr *MockSignatureRepositoryMockRecorder) GetCorporateSignature(ctx, claGroupID, companyID, approved, signed interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCorporateSignature", reflect.TypeOf((*MockSignatureRepository)(nil).GetCorporateSignature), ctx, claGroupID, companyID, approved, signed) +} + +// GetCorporateSignatures mocks base method. +func (m *MockSignatureRepository) GetCorporateSignatures(ctx context.Context, claGroupID, companyID string, approved, signed *bool) ([]*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCorporateSignatures", ctx, claGroupID, companyID, approved, signed) + ret0, _ := ret[0].([]*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCorporateSignatures indicates an expected call of GetCorporateSignatures. +func (mr *MockSignatureRepositoryMockRecorder) GetCorporateSignatures(ctx, claGroupID, companyID, approved, signed interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCorporateSignatures", reflect.TypeOf((*MockSignatureRepository)(nil).GetCorporateSignatures), ctx, claGroupID, companyID, approved, signed) +} + +// GetGithubOrganizationsFromApprovalList mocks base method. +func (m *MockSignatureRepository) GetGithubOrganizationsFromApprovalList(ctx context.Context, signatureID string) ([]models.GithubOrg, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGithubOrganizationsFromApprovalList", ctx, signatureID) + ret0, _ := ret[0].([]models.GithubOrg) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetGithubOrganizationsFromApprovalList indicates an expected call of GetGithubOrganizationsFromApprovalList. +func (mr *MockSignatureRepositoryMockRecorder) GetGithubOrganizationsFromApprovalList(ctx, signatureID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGithubOrganizationsFromApprovalList", reflect.TypeOf((*MockSignatureRepository)(nil).GetGithubOrganizationsFromApprovalList), ctx, signatureID) +} + +// GetICLAByDate mocks base method. +func (m *MockSignatureRepository) GetICLAByDate(ctx context.Context, startDate string) ([]signatures0.ItemSignature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetICLAByDate", ctx, startDate) + ret0, _ := ret[0].([]signatures0.ItemSignature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetICLAByDate indicates an expected call of GetICLAByDate. +func (mr *MockSignatureRepositoryMockRecorder) GetICLAByDate(ctx, startDate interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetICLAByDate", reflect.TypeOf((*MockSignatureRepository)(nil).GetICLAByDate), ctx, startDate) +} + +// GetIndividualSignature mocks base method. +func (m *MockSignatureRepository) GetIndividualSignature(ctx context.Context, claGroupID, userID string, approved, signed *bool) (*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetIndividualSignature", ctx, claGroupID, userID, approved, signed) + ret0, _ := ret[0].(*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetIndividualSignature indicates an expected call of GetIndividualSignature. +func (mr *MockSignatureRepositoryMockRecorder) GetIndividualSignature(ctx, claGroupID, userID, approved, signed interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIndividualSignature", reflect.TypeOf((*MockSignatureRepository)(nil).GetIndividualSignature), ctx, claGroupID, userID, approved, signed) +} + +// GetIndividualSignatures mocks base method. +func (m *MockSignatureRepository) GetIndividualSignatures(ctx context.Context, claGroupID, userID string, approved, signed *bool) ([]*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetIndividualSignatures", ctx, claGroupID, userID, approved, signed) + ret0, _ := ret[0].([]*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetIndividualSignatures indicates an expected call of GetIndividualSignatures. +func (mr *MockSignatureRepositoryMockRecorder) GetIndividualSignatures(ctx, claGroupID, userID, approved, signed interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIndividualSignatures", reflect.TypeOf((*MockSignatureRepository)(nil).GetIndividualSignatures), ctx, claGroupID, userID, approved, signed) +} + +// GetItemSignature mocks base method. +func (m *MockSignatureRepository) GetItemSignature(ctx context.Context, signatureID string) (*signatures0.ItemSignature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetItemSignature", ctx, signatureID) + ret0, _ := ret[0].(*signatures0.ItemSignature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetItemSignature indicates an expected call of GetItemSignature. +func (mr *MockSignatureRepositoryMockRecorder) GetItemSignature(ctx, signatureID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItemSignature", reflect.TypeOf((*MockSignatureRepository)(nil).GetItemSignature), ctx, signatureID) +} + +// GetProjectCompanyEmployeeSignature mocks base method. +func (m *MockSignatureRepository) GetProjectCompanyEmployeeSignature(ctx context.Context, companyModel *models.Company, claGroupModel *models.ClaGroup, employeeUserModel *models.User, wg *sync.WaitGroup, resultChannel chan<- *signatures0.EmployeeModel, errorChannel chan<- error) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "GetProjectCompanyEmployeeSignature", ctx, companyModel, claGroupModel, employeeUserModel, wg, resultChannel, errorChannel) +} + +// GetProjectCompanyEmployeeSignature indicates an expected call of GetProjectCompanyEmployeeSignature. +func (mr *MockSignatureRepositoryMockRecorder) GetProjectCompanyEmployeeSignature(ctx, companyModel, claGroupModel, employeeUserModel, wg, resultChannel, errorChannel interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProjectCompanyEmployeeSignature", reflect.TypeOf((*MockSignatureRepository)(nil).GetProjectCompanyEmployeeSignature), ctx, companyModel, claGroupModel, employeeUserModel, wg, resultChannel, errorChannel) +} + +// GetProjectCompanyEmployeeSignatures mocks base method. +func (m *MockSignatureRepository) GetProjectCompanyEmployeeSignatures(ctx context.Context, params signatures.GetProjectCompanyEmployeeSignaturesParams, criteria *signatures0.ApprovalCriteria) (*models.Signatures, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProjectCompanyEmployeeSignatures", ctx, params, criteria) + ret0, _ := ret[0].(*models.Signatures) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProjectCompanyEmployeeSignatures indicates an expected call of GetProjectCompanyEmployeeSignatures. +func (mr *MockSignatureRepositoryMockRecorder) GetProjectCompanyEmployeeSignatures(ctx, params, criteria interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProjectCompanyEmployeeSignatures", reflect.TypeOf((*MockSignatureRepository)(nil).GetProjectCompanyEmployeeSignatures), ctx, params, criteria) +} + +// GetProjectCompanySignature mocks base method. +func (m *MockSignatureRepository) GetProjectCompanySignature(ctx context.Context, companyID, projectID string, approved, signed *bool, nextKey *string, pageSize *int64) (*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProjectCompanySignature", ctx, companyID, projectID, approved, signed, nextKey, pageSize) + ret0, _ := ret[0].(*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProjectCompanySignature indicates an expected call of GetProjectCompanySignature. +func (mr *MockSignatureRepositoryMockRecorder) GetProjectCompanySignature(ctx, companyID, projectID, approved, signed, nextKey, pageSize interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProjectCompanySignature", reflect.TypeOf((*MockSignatureRepository)(nil).GetProjectCompanySignature), ctx, companyID, projectID, approved, signed, nextKey, pageSize) +} + +// GetProjectCompanySignatures mocks base method. +func (m *MockSignatureRepository) GetProjectCompanySignatures(ctx context.Context, companyID, projectID string, approved, signed *bool, nextKey, sortOrder *string, pageSize *int64) (*models.Signatures, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProjectCompanySignatures", ctx, companyID, projectID, approved, signed, nextKey, sortOrder, pageSize) + ret0, _ := ret[0].(*models.Signatures) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProjectCompanySignatures indicates an expected call of GetProjectCompanySignatures. +func (mr *MockSignatureRepositoryMockRecorder) GetProjectCompanySignatures(ctx, companyID, projectID, approved, signed, nextKey, sortOrder, pageSize interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProjectCompanySignatures", reflect.TypeOf((*MockSignatureRepository)(nil).GetProjectCompanySignatures), ctx, companyID, projectID, approved, signed, nextKey, sortOrder, pageSize) +} + +// GetProjectSignatures mocks base method. +func (m *MockSignatureRepository) GetProjectSignatures(ctx context.Context, params signatures.GetProjectSignaturesParams) (*models.Signatures, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProjectSignatures", ctx, params) + ret0, _ := ret[0].(*models.Signatures) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProjectSignatures indicates an expected call of GetProjectSignatures. +func (mr *MockSignatureRepositoryMockRecorder) GetProjectSignatures(ctx, params interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProjectSignatures", reflect.TypeOf((*MockSignatureRepository)(nil).GetProjectSignatures), ctx, params) +} + +// GetSignature mocks base method. +func (m *MockSignatureRepository) GetSignature(ctx context.Context, signatureID string) (*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSignature", ctx, signatureID) + ret0, _ := ret[0].(*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSignature indicates an expected call of GetSignature. +func (mr *MockSignatureRepositoryMockRecorder) GetSignature(ctx, signatureID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSignature", reflect.TypeOf((*MockSignatureRepository)(nil).GetSignature), ctx, signatureID) +} + +// GetSignatureACL mocks base method. +func (m *MockSignatureRepository) GetSignatureACL(ctx context.Context, signatureID string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSignatureACL", ctx, signatureID) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSignatureACL indicates an expected call of GetSignatureACL. +func (mr *MockSignatureRepositoryMockRecorder) GetSignatureACL(ctx, signatureID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSignatureACL", reflect.TypeOf((*MockSignatureRepository)(nil).GetSignatureACL), ctx, signatureID) +} + +// GetUserSignatures mocks base method. +func (m *MockSignatureRepository) GetUserSignatures(ctx context.Context, params signatures.GetUserSignaturesParams, pageSize int64, projectID *string) (*models.Signatures, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserSignatures", ctx, params, pageSize, projectID) + ret0, _ := ret[0].(*models.Signatures) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserSignatures indicates an expected call of GetUserSignatures. +func (mr *MockSignatureRepositoryMockRecorder) GetUserSignatures(ctx, params, pageSize, projectID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserSignatures", reflect.TypeOf((*MockSignatureRepository)(nil).GetUserSignatures), ctx, params, pageSize, projectID) +} + +// InvalidateProjectRecord mocks base method. +func (m *MockSignatureRepository) InvalidateProjectRecord(ctx context.Context, signatureID, note string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InvalidateProjectRecord", ctx, signatureID, note) + ret0, _ := ret[0].(error) + return ret0 +} + +// InvalidateProjectRecord indicates an expected call of InvalidateProjectRecord. +func (mr *MockSignatureRepositoryMockRecorder) InvalidateProjectRecord(ctx, signatureID, note interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InvalidateProjectRecord", reflect.TypeOf((*MockSignatureRepository)(nil).InvalidateProjectRecord), ctx, signatureID, note) +} + +// ProjectSignatures mocks base method. +func (m *MockSignatureRepository) ProjectSignatures(ctx context.Context, projectID string) (*models.Signatures, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProjectSignatures", ctx, projectID) + ret0, _ := ret[0].(*models.Signatures) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ProjectSignatures indicates an expected call of ProjectSignatures. +func (mr *MockSignatureRepositoryMockRecorder) ProjectSignatures(ctx, projectID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectSignatures", reflect.TypeOf((*MockSignatureRepository)(nil).ProjectSignatures), ctx, projectID) +} + +// RemoveCLAManager mocks base method. +func (m *MockSignatureRepository) RemoveCLAManager(ctx context.Context, signatureID, claManagerID string) (*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveCLAManager", ctx, signatureID, claManagerID) + ret0, _ := ret[0].(*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RemoveCLAManager indicates an expected call of RemoveCLAManager. +func (mr *MockSignatureRepositoryMockRecorder) RemoveCLAManager(ctx, signatureID, claManagerID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveCLAManager", reflect.TypeOf((*MockSignatureRepository)(nil).RemoveCLAManager), ctx, signatureID, claManagerID) +} + +// SaveOrUpdateSignature mocks base method. +func (m *MockSignatureRepository) SaveOrUpdateSignature(ctx context.Context, signature *signatures0.ItemSignature) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveOrUpdateSignature", ctx, signature) + ret0, _ := ret[0].(error) + return ret0 +} + +// SaveOrUpdateSignature indicates an expected call of SaveOrUpdateSignature. +func (mr *MockSignatureRepositoryMockRecorder) SaveOrUpdateSignature(ctx, signature interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveOrUpdateSignature", reflect.TypeOf((*MockSignatureRepository)(nil).SaveOrUpdateSignature), ctx, signature) +} + +// UpdateApprovalList mocks base method. +func (m *MockSignatureRepository) UpdateApprovalList(ctx context.Context, claManager *models.User, claGroupModel *models.ClaGroup, companyID string, params *models.ApprovalList, eventArgs *events.LogEventArgs) (*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateApprovalList", ctx, claManager, claGroupModel, companyID, params, eventArgs) + ret0, _ := ret[0].(*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateApprovalList indicates an expected call of UpdateApprovalList. +func (mr *MockSignatureRepositoryMockRecorder) UpdateApprovalList(ctx, claManager, claGroupModel, companyID, params, eventArgs interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateApprovalList", reflect.TypeOf((*MockSignatureRepository)(nil).UpdateApprovalList), ctx, claManager, claGroupModel, companyID, params, eventArgs) +} + +// UpdateEnvelopeDetails mocks base method. +func (m *MockSignatureRepository) UpdateEnvelopeDetails(ctx context.Context, signatureID, envelopeID string, signURL *string) (*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateEnvelopeDetails", ctx, signatureID, envelopeID, signURL) + ret0, _ := ret[0].(*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateEnvelopeDetails indicates an expected call of UpdateEnvelopeDetails. +func (mr *MockSignatureRepositoryMockRecorder) UpdateEnvelopeDetails(ctx, signatureID, envelopeID, signURL interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEnvelopeDetails", reflect.TypeOf((*MockSignatureRepository)(nil).UpdateEnvelopeDetails), ctx, signatureID, envelopeID, signURL) +} + +// UpdateSignature mocks base method. +func (m *MockSignatureRepository) UpdateSignature(ctx context.Context, signatureID string, updates map[string]interface{}) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateSignature", ctx, signatureID, updates) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateSignature indicates an expected call of UpdateSignature. +func (mr *MockSignatureRepositoryMockRecorder) UpdateSignature(ctx, signatureID, updates interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSignature", reflect.TypeOf((*MockSignatureRepository)(nil).UpdateSignature), ctx, signatureID, updates) +} + +// ValidateProjectRecord mocks base method. +func (m *MockSignatureRepository) ValidateProjectRecord(ctx context.Context, signatureID, note string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateProjectRecord", ctx, signatureID, note) + ret0, _ := ret[0].(error) + return ret0 +} + +// ValidateProjectRecord indicates an expected call of ValidateProjectRecord. +func (mr *MockSignatureRepositoryMockRecorder) ValidateProjectRecord(ctx, signatureID, note interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateProjectRecord", reflect.TypeOf((*MockSignatureRepository)(nil).ValidateProjectRecord), ctx, signatureID, note) +} diff --git a/cla-backend-go/signatures/mocks/mock_service.go b/cla-backend-go/signatures/mocks/mock_service.go new file mode 100644 index 000000000..1f70a6caf --- /dev/null +++ b/cla-backend-go/signatures/mocks/mock_service.go @@ -0,0 +1,520 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +// Code generated by MockGen. DO NOT EDIT. +// Source: signatures/service.go + +// Package mock_signatures is a generated GoMock package. +package mock_signatures + +import ( + context "context" + reflect "reflect" + + auth "github.com/LF-Engineering/lfx-kit/auth" + models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + signatures "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/signatures" + signatures0 "github.com/communitybridge/easycla/cla-backend-go/signatures" + gomock "github.com/golang/mock/gomock" +) + +// MockSignatureService is a mock of SignatureService interface. +type MockSignatureService struct { + ctrl *gomock.Controller + recorder *MockSignatureServiceMockRecorder +} + +// MockSignatureServiceMockRecorder is the mock recorder for MockSignatureService. +type MockSignatureServiceMockRecorder struct { + mock *MockSignatureService +} + +// NewMockSignatureService creates a new mock instance. +func NewMockSignatureService(ctrl *gomock.Controller) *MockSignatureService { + mock := &MockSignatureService{ctrl: ctrl} + mock.recorder = &MockSignatureServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSignatureService) EXPECT() *MockSignatureServiceMockRecorder { + return m.recorder +} + +// AddCLAManager mocks base method. +func (m *MockSignatureService) AddCLAManager(ctx context.Context, signatureID, claManagerID string) (*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddCLAManager", ctx, signatureID, claManagerID) + ret0, _ := ret[0].(*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddCLAManager indicates an expected call of AddCLAManager. +func (mr *MockSignatureServiceMockRecorder) AddCLAManager(ctx, signatureID, claManagerID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddCLAManager", reflect.TypeOf((*MockSignatureService)(nil).AddCLAManager), ctx, signatureID, claManagerID) +} + +// AddGithubOrganizationToApprovalList mocks base method. +func (m *MockSignatureService) AddGithubOrganizationToApprovalList(ctx context.Context, signatureID string, approvalListParams models.GhOrgWhitelist, githubAccessToken string) ([]models.GithubOrg, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddGithubOrganizationToApprovalList", ctx, signatureID, approvalListParams, githubAccessToken) + ret0, _ := ret[0].([]models.GithubOrg) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddGithubOrganizationToApprovalList indicates an expected call of AddGithubOrganizationToApprovalList. +func (mr *MockSignatureServiceMockRecorder) AddGithubOrganizationToApprovalList(ctx, signatureID, approvalListParams, githubAccessToken interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGithubOrganizationToApprovalList", reflect.TypeOf((*MockSignatureService)(nil).AddGithubOrganizationToApprovalList), ctx, signatureID, approvalListParams, githubAccessToken) +} + +// CreateOrUpdateEmployeeSignature mocks base method. +func (m *MockSignatureService) CreateOrUpdateEmployeeSignature(ctx context.Context, claGroupModel *models.ClaGroup, companyModel *models.Company, corporateSignatureModel *models.Signature) ([]*models.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateOrUpdateEmployeeSignature", ctx, claGroupModel, companyModel, corporateSignatureModel) + ret0, _ := ret[0].([]*models.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateOrUpdateEmployeeSignature indicates an expected call of CreateOrUpdateEmployeeSignature. +func (mr *MockSignatureServiceMockRecorder) CreateOrUpdateEmployeeSignature(ctx, claGroupModel, companyModel, corporateSignatureModel interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateEmployeeSignature", reflect.TypeOf((*MockSignatureService)(nil).CreateOrUpdateEmployeeSignature), ctx, claGroupModel, companyModel, corporateSignatureModel) +} + +// CreateProjectSummaryReport mocks base method. +func (m *MockSignatureService) CreateProjectSummaryReport(ctx context.Context, params signatures.CreateProjectSummaryReportParams) (*models.SignatureReport, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateProjectSummaryReport", ctx, params) + ret0, _ := ret[0].(*models.SignatureReport) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateProjectSummaryReport indicates an expected call of CreateProjectSummaryReport. +func (mr *MockSignatureServiceMockRecorder) CreateProjectSummaryReport(ctx, params interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateProjectSummaryReport", reflect.TypeOf((*MockSignatureService)(nil).CreateProjectSummaryReport), ctx, params) +} + +// CreateSignature mocks base method. +func (m *MockSignatureService) CreateSignature(ctx context.Context, signature *signatures0.ItemSignature) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateSignature", ctx, signature) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateSignature indicates an expected call of CreateSignature. +func (mr *MockSignatureServiceMockRecorder) CreateSignature(ctx, signature interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSignature", reflect.TypeOf((*MockSignatureService)(nil).CreateSignature), ctx, signature) +} + +// DeleteGithubOrganizationFromApprovalList mocks base method. +func (m *MockSignatureService) DeleteGithubOrganizationFromApprovalList(ctx context.Context, signatureID string, approvalListParams models.GhOrgWhitelist, githubAccessToken string) ([]models.GithubOrg, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteGithubOrganizationFromApprovalList", ctx, signatureID, approvalListParams, githubAccessToken) + ret0, _ := ret[0].([]models.GithubOrg) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteGithubOrganizationFromApprovalList indicates an expected call of DeleteGithubOrganizationFromApprovalList. +func (mr *MockSignatureServiceMockRecorder) DeleteGithubOrganizationFromApprovalList(ctx, signatureID, approvalListParams, githubAccessToken interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteGithubOrganizationFromApprovalList", reflect.TypeOf((*MockSignatureService)(nil).DeleteGithubOrganizationFromApprovalList), ctx, signatureID, approvalListParams, githubAccessToken) +} + +// GetCCLASignatures mocks base method. +func (m *MockSignatureService) GetCCLASignatures(ctx context.Context, signed, approved *bool) ([]*signatures0.ItemSignature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCCLASignatures", ctx, signed, approved) + ret0, _ := ret[0].([]*signatures0.ItemSignature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCCLASignatures indicates an expected call of GetCCLASignatures. +func (mr *MockSignatureServiceMockRecorder) GetCCLASignatures(ctx, signed, approved interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCCLASignatures", reflect.TypeOf((*MockSignatureService)(nil).GetCCLASignatures), ctx, signed, approved) +} + +// GetClaGroupCCLASignatures mocks base method. +func (m *MockSignatureService) GetClaGroupCCLASignatures(ctx context.Context, claGroupID string, approved, signed *bool) (*models.Signatures, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClaGroupCCLASignatures", ctx, claGroupID, approved, signed) + ret0, _ := ret[0].(*models.Signatures) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClaGroupCCLASignatures indicates an expected call of GetClaGroupCCLASignatures. +func (mr *MockSignatureServiceMockRecorder) GetClaGroupCCLASignatures(ctx, claGroupID, approved, signed interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaGroupCCLASignatures", reflect.TypeOf((*MockSignatureService)(nil).GetClaGroupCCLASignatures), ctx, claGroupID, approved, signed) +} + +// GetClaGroupCorporateContributors mocks base method. +func (m *MockSignatureService) GetClaGroupCorporateContributors(ctx context.Context, claGroupID string, companyID *string, pageSize *int64, nextKey, searchTerm *string) (*models.CorporateContributorList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClaGroupCorporateContributors", ctx, claGroupID, companyID, pageSize, nextKey, searchTerm) + ret0, _ := ret[0].(*models.CorporateContributorList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClaGroupCorporateContributors indicates an expected call of GetClaGroupCorporateContributors. +func (mr *MockSignatureServiceMockRecorder) GetClaGroupCorporateContributors(ctx, claGroupID, companyID, pageSize, nextKey, searchTerm interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaGroupCorporateContributors", reflect.TypeOf((*MockSignatureService)(nil).GetClaGroupCorporateContributors), ctx, claGroupID, companyID, pageSize, nextKey, searchTerm) +} + +// GetClaGroupICLASignatures mocks base method. +func (m *MockSignatureService) GetClaGroupICLASignatures(ctx context.Context, claGroupID string, searchTerm *string, approved, signed *bool, pageSize int64, nextKey string, withExtraDetails bool) (*models.IclaSignatures, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClaGroupICLASignatures", ctx, claGroupID, searchTerm, approved, signed, pageSize, nextKey, withExtraDetails) + ret0, _ := ret[0].(*models.IclaSignatures) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClaGroupICLASignatures indicates an expected call of GetClaGroupICLASignatures. +func (mr *MockSignatureServiceMockRecorder) GetClaGroupICLASignatures(ctx, claGroupID, searchTerm, approved, signed, pageSize, nextKey, withExtraDetails interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaGroupICLASignatures", reflect.TypeOf((*MockSignatureService)(nil).GetClaGroupICLASignatures), ctx, claGroupID, searchTerm, approved, signed, pageSize, nextKey, withExtraDetails) +} + +// GetCompanyIDsWithSignedCorporateSignatures mocks base method. +func (m *MockSignatureService) GetCompanyIDsWithSignedCorporateSignatures(ctx context.Context, claGroupID string) ([]signatures0.SignatureCompanyID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanyIDsWithSignedCorporateSignatures", ctx, claGroupID) + ret0, _ := ret[0].([]signatures0.SignatureCompanyID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanyIDsWithSignedCorporateSignatures indicates an expected call of GetCompanyIDsWithSignedCorporateSignatures. +func (mr *MockSignatureServiceMockRecorder) GetCompanyIDsWithSignedCorporateSignatures(ctx, claGroupID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanyIDsWithSignedCorporateSignatures", reflect.TypeOf((*MockSignatureService)(nil).GetCompanyIDsWithSignedCorporateSignatures), ctx, claGroupID) +} + +// GetCompanySignatures mocks base method. +func (m *MockSignatureService) GetCompanySignatures(ctx context.Context, params signatures.GetCompanySignaturesParams) (*models.Signatures, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanySignatures", ctx, params) + ret0, _ := ret[0].(*models.Signatures) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanySignatures indicates an expected call of GetCompanySignatures. +func (mr *MockSignatureServiceMockRecorder) GetCompanySignatures(ctx, params interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanySignatures", reflect.TypeOf((*MockSignatureService)(nil).GetCompanySignatures), ctx, params) +} + +// GetCorporateSignature mocks base method. +func (m *MockSignatureService) GetCorporateSignature(ctx context.Context, claGroupID, companyID string, approved, signed *bool) (*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCorporateSignature", ctx, claGroupID, companyID, approved, signed) + ret0, _ := ret[0].(*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCorporateSignature indicates an expected call of GetCorporateSignature. +func (mr *MockSignatureServiceMockRecorder) GetCorporateSignature(ctx, claGroupID, companyID, approved, signed interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCorporateSignature", reflect.TypeOf((*MockSignatureService)(nil).GetCorporateSignature), ctx, claGroupID, companyID, approved, signed) +} + +// GetCorporateSignatures mocks base method. +func (m *MockSignatureService) GetCorporateSignatures(ctx context.Context, claGroupID, companyID string, approved, signed *bool) ([]*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCorporateSignatures", ctx, claGroupID, companyID, approved, signed) + ret0, _ := ret[0].([]*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCorporateSignatures indicates an expected call of GetCorporateSignatures. +func (mr *MockSignatureServiceMockRecorder) GetCorporateSignatures(ctx, claGroupID, companyID, approved, signed interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCorporateSignatures", reflect.TypeOf((*MockSignatureService)(nil).GetCorporateSignatures), ctx, claGroupID, companyID, approved, signed) +} + +// GetGithubOrganizationsFromApprovalList mocks base method. +func (m *MockSignatureService) GetGithubOrganizationsFromApprovalList(ctx context.Context, signatureID, githubAccessToken string) ([]models.GithubOrg, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGithubOrganizationsFromApprovalList", ctx, signatureID, githubAccessToken) + ret0, _ := ret[0].([]models.GithubOrg) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetGithubOrganizationsFromApprovalList indicates an expected call of GetGithubOrganizationsFromApprovalList. +func (mr *MockSignatureServiceMockRecorder) GetGithubOrganizationsFromApprovalList(ctx, signatureID, githubAccessToken interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGithubOrganizationsFromApprovalList", reflect.TypeOf((*MockSignatureService)(nil).GetGithubOrganizationsFromApprovalList), ctx, signatureID, githubAccessToken) +} + +// GetIndividualSignature mocks base method. +func (m *MockSignatureService) GetIndividualSignature(ctx context.Context, claGroupID, userID string, approved, signed *bool) (*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetIndividualSignature", ctx, claGroupID, userID, approved, signed) + ret0, _ := ret[0].(*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetIndividualSignature indicates an expected call of GetIndividualSignature. +func (mr *MockSignatureServiceMockRecorder) GetIndividualSignature(ctx, claGroupID, userID, approved, signed interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIndividualSignature", reflect.TypeOf((*MockSignatureService)(nil).GetIndividualSignature), ctx, claGroupID, userID, approved, signed) +} + +// GetIndividualSignatures mocks base method. +func (m *MockSignatureService) GetIndividualSignatures(ctx context.Context, claGroupID, userID string, approved, signed *bool) ([]*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetIndividualSignatures", ctx, claGroupID, userID, approved, signed) + ret0, _ := ret[0].([]*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetIndividualSignatures indicates an expected call of GetIndividualSignatures. +func (mr *MockSignatureServiceMockRecorder) GetIndividualSignatures(ctx, claGroupID, userID, approved, signed interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIndividualSignatures", reflect.TypeOf((*MockSignatureService)(nil).GetIndividualSignatures), ctx, claGroupID, userID, approved, signed) +} + +// GetProjectCompanyEmployeeSignatures mocks base method. +func (m *MockSignatureService) GetProjectCompanyEmployeeSignatures(ctx context.Context, params signatures.GetProjectCompanyEmployeeSignaturesParams, criteria *signatures0.ApprovalCriteria) (*models.Signatures, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProjectCompanyEmployeeSignatures", ctx, params, criteria) + ret0, _ := ret[0].(*models.Signatures) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProjectCompanyEmployeeSignatures indicates an expected call of GetProjectCompanyEmployeeSignatures. +func (mr *MockSignatureServiceMockRecorder) GetProjectCompanyEmployeeSignatures(ctx, params, criteria interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProjectCompanyEmployeeSignatures", reflect.TypeOf((*MockSignatureService)(nil).GetProjectCompanyEmployeeSignatures), ctx, params, criteria) +} + +// GetProjectCompanySignature mocks base method. +func (m *MockSignatureService) GetProjectCompanySignature(ctx context.Context, companyID, projectID string, approved, signed *bool, nextKey *string, pageSize *int64) (*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProjectCompanySignature", ctx, companyID, projectID, approved, signed, nextKey, pageSize) + ret0, _ := ret[0].(*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProjectCompanySignature indicates an expected call of GetProjectCompanySignature. +func (mr *MockSignatureServiceMockRecorder) GetProjectCompanySignature(ctx, companyID, projectID, approved, signed, nextKey, pageSize interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProjectCompanySignature", reflect.TypeOf((*MockSignatureService)(nil).GetProjectCompanySignature), ctx, companyID, projectID, approved, signed, nextKey, pageSize) +} + +// GetProjectCompanySignatures mocks base method. +func (m *MockSignatureService) GetProjectCompanySignatures(ctx context.Context, params signatures.GetProjectCompanySignaturesParams) (*models.Signatures, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProjectCompanySignatures", ctx, params) + ret0, _ := ret[0].(*models.Signatures) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProjectCompanySignatures indicates an expected call of GetProjectCompanySignatures. +func (mr *MockSignatureServiceMockRecorder) GetProjectCompanySignatures(ctx, params interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProjectCompanySignatures", reflect.TypeOf((*MockSignatureService)(nil).GetProjectCompanySignatures), ctx, params) +} + +// GetProjectSignatures mocks base method. +func (m *MockSignatureService) GetProjectSignatures(ctx context.Context, params signatures.GetProjectSignaturesParams) (*models.Signatures, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProjectSignatures", ctx, params) + ret0, _ := ret[0].(*models.Signatures) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProjectSignatures indicates an expected call of GetProjectSignatures. +func (mr *MockSignatureServiceMockRecorder) GetProjectSignatures(ctx, params interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProjectSignatures", reflect.TypeOf((*MockSignatureService)(nil).GetProjectSignatures), ctx, params) +} + +// GetSignature mocks base method. +func (m *MockSignatureService) GetSignature(ctx context.Context, signatureID string) (*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSignature", ctx, signatureID) + ret0, _ := ret[0].(*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSignature indicates an expected call of GetSignature. +func (mr *MockSignatureServiceMockRecorder) GetSignature(ctx, signatureID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSignature", reflect.TypeOf((*MockSignatureService)(nil).GetSignature), ctx, signatureID) +} + +// GetUserSignatures mocks base method. +func (m *MockSignatureService) GetUserSignatures(ctx context.Context, params signatures.GetUserSignaturesParams, projectID *string) (*models.Signatures, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserSignatures", ctx, params, projectID) + ret0, _ := ret[0].(*models.Signatures) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserSignatures indicates an expected call of GetUserSignatures. +func (mr *MockSignatureServiceMockRecorder) GetUserSignatures(ctx, params, projectID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserSignatures", reflect.TypeOf((*MockSignatureService)(nil).GetUserSignatures), ctx, params, projectID) +} + +// HasUserSigned mocks base method. +func (m *MockSignatureService) HasUserSigned(ctx context.Context, user *models.User, projectID string) (*bool, *bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HasUserSigned", ctx, user, projectID) + ret0, _ := ret[0].(*bool) + ret1, _ := ret[1].(*bool) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// HasUserSigned indicates an expected call of HasUserSigned. +func (mr *MockSignatureServiceMockRecorder) HasUserSigned(ctx, user, projectID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasUserSigned", reflect.TypeOf((*MockSignatureService)(nil).HasUserSigned), ctx, user, projectID) +} + +// InvalidateProjectRecords mocks base method. +func (m *MockSignatureService) InvalidateProjectRecords(ctx context.Context, projectID, note string) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InvalidateProjectRecords", ctx, projectID, note) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InvalidateProjectRecords indicates an expected call of InvalidateProjectRecords. +func (mr *MockSignatureServiceMockRecorder) InvalidateProjectRecords(ctx, projectID, note interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InvalidateProjectRecords", reflect.TypeOf((*MockSignatureService)(nil).InvalidateProjectRecords), ctx, projectID, note) +} + +// ProcessEmployeeSignature mocks base method. +func (m *MockSignatureService) ProcessEmployeeSignature(ctx context.Context, companyModel *models.Company, claGroupModel *models.ClaGroup, user *models.User) (*bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProcessEmployeeSignature", ctx, companyModel, claGroupModel, user) + ret0, _ := ret[0].(*bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ProcessEmployeeSignature indicates an expected call of ProcessEmployeeSignature. +func (mr *MockSignatureServiceMockRecorder) ProcessEmployeeSignature(ctx, companyModel, claGroupModel, user interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessEmployeeSignature", reflect.TypeOf((*MockSignatureService)(nil).ProcessEmployeeSignature), ctx, companyModel, claGroupModel, user) +} + +// RemoveCLAManager mocks base method. +func (m *MockSignatureService) RemoveCLAManager(ctx context.Context, ignatureID, claManagerID string) (*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveCLAManager", ctx, ignatureID, claManagerID) + ret0, _ := ret[0].(*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RemoveCLAManager indicates an expected call of RemoveCLAManager. +func (mr *MockSignatureServiceMockRecorder) RemoveCLAManager(ctx, ignatureID, claManagerID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveCLAManager", reflect.TypeOf((*MockSignatureService)(nil).RemoveCLAManager), ctx, ignatureID, claManagerID) +} + +// SaveOrUpdateSignature mocks base method. +func (m *MockSignatureService) SaveOrUpdateSignature(ctx context.Context, signature *signatures0.ItemSignature) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveOrUpdateSignature", ctx, signature) + ret0, _ := ret[0].(error) + return ret0 +} + +// SaveOrUpdateSignature indicates an expected call of SaveOrUpdateSignature. +func (mr *MockSignatureServiceMockRecorder) SaveOrUpdateSignature(ctx, signature interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveOrUpdateSignature", reflect.TypeOf((*MockSignatureService)(nil).SaveOrUpdateSignature), ctx, signature) +} + +// UpdateApprovalList mocks base method. +func (m *MockSignatureService) UpdateApprovalList(ctx context.Context, authUser *auth.User, claGroupModel *models.ClaGroup, companyModel *models.Company, claGroupID string, params *models.ApprovalList, projectSFID string) (*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateApprovalList", ctx, authUser, claGroupModel, companyModel, claGroupID, params, projectSFID) + ret0, _ := ret[0].(*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateApprovalList indicates an expected call of UpdateApprovalList. +func (mr *MockSignatureServiceMockRecorder) UpdateApprovalList(ctx, authUser, claGroupModel, companyModel, claGroupID, params, projectSFID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateApprovalList", reflect.TypeOf((*MockSignatureService)(nil).UpdateApprovalList), ctx, authUser, claGroupModel, companyModel, claGroupID, params, projectSFID) +} + +// UpdateEnvelopeDetails mocks base method. +func (m *MockSignatureService) UpdateEnvelopeDetails(ctx context.Context, signatureID, envelopeID string, signURL *string) (*models.Signature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateEnvelopeDetails", ctx, signatureID, envelopeID, signURL) + ret0, _ := ret[0].(*models.Signature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateEnvelopeDetails indicates an expected call of UpdateEnvelopeDetails. +func (mr *MockSignatureServiceMockRecorder) UpdateEnvelopeDetails(ctx, signatureID, envelopeID, signURL interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEnvelopeDetails", reflect.TypeOf((*MockSignatureService)(nil).UpdateEnvelopeDetails), ctx, signatureID, envelopeID, signURL) +} + +// UpdateSignature mocks base method. +func (m *MockSignatureService) UpdateSignature(ctx context.Context, signatureID string, updates map[string]interface{}) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateSignature", ctx, signatureID, updates) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateSignature indicates an expected call of UpdateSignature. +func (mr *MockSignatureServiceMockRecorder) UpdateSignature(ctx, signatureID, updates interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSignature", reflect.TypeOf((*MockSignatureService)(nil).UpdateSignature), ctx, signatureID, updates) +} + +// UserIsApproved mocks base method. +func (m *MockSignatureService) UserIsApproved(ctx context.Context, user *models.User, cclaSignature *models.Signature) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UserIsApproved", ctx, user, cclaSignature) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UserIsApproved indicates an expected call of UserIsApproved. +func (mr *MockSignatureServiceMockRecorder) UserIsApproved(ctx, user, cclaSignature interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserIsApproved", reflect.TypeOf((*MockSignatureService)(nil).UserIsApproved), ctx, user, cclaSignature) +} diff --git a/cla-backend-go/signatures/models.go b/cla-backend-go/signatures/models.go index 6684e0be9..006aedade 100644 --- a/cla-backend-go/signatures/models.go +++ b/cla-backend-go/signatures/models.go @@ -3,6 +3,20 @@ package signatures +import ( + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + v2Models "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" +) + +// simpleUserInfoModel is a simple/temp user model to consolidate the email list, GitHub username list, and GitLab username list +type simpleUserInfoModel struct { + Email string + GitHubUserName string + GitHubUserID string + GitLabUserName string + GitLabUserID string +} + // SignatureCompanyID is a simple data model to hold the signature ID and come company details for CCLA's type SignatureCompanyID struct { SignatureID string @@ -10,3 +24,111 @@ type SignatureCompanyID struct { CompanySFID string CompanyName string } + +// ApprovalCriteria struct representing approval criteria values +type ApprovalCriteria struct { + UserEmail string + GitHubUsername string + GitlabUsername string +} + +// ApprovalList data model +type ApprovalList struct { + Criteria string + ApprovalList []string + Action string + ClaGroupID string + ClaGroupName string + CompanyID string + Version string + EmailApprovals []string + DomainApprovals []string + GitHubUsernameApprovals []string + GitHubUsernames []string + GitHubOrgApprovals []string + GitlabUsernameApprovals []string + GitlabOrgApprovals []string + GitlabUsernames []string + GerritICLAECLAs []string + ICLAs []*models.IclaSignature + ECLAs []*models.Signature + CLAManager *models.User + ManagersInfo []ClaManagerInfoParams + CCLASignature *models.Signature +} + +// GerritUserResponse is a data structure to hold the gerrit user query response +type GerritUserResponse struct { + gerritGroupResponse *v2Models.GerritGroupResponse + queryType string + Error error +} + +// ICLAUserResponse is struct that supports ICLAUsers +type ICLAUserResponse struct { + ICLASignature *models.IclaSignature + Error error +} + +const ( + //CCLAICLA representing user removal under CCLA + ICLA + CCLAICLA = "CCLAICLA" + //CCLAICLAECLA representing user removal under CCLA + ICLA +ECLA + CCLAICLAECLA = "CCLAICLAECLA" + //CCLA representing normal use case of user under CCLA + CCLA = "ICLA" + //ICLA representing individual use case + ICLA = "ICLA" +) + +// SignatureDynamoDB is a data model for the signature table. Most of the record create/update happens in the old +// Python code, however, we needed to add this data model after we added the auto-enable feature for employee acknowledgements. +// +// | Type of Signature | `project_id` |`signature_reference_type`|`signature_type`|`signature_reference_id`|`signature_user_ccla_company_id`| PDF? | Auto Create ECLA Flag | +// |:-----------------------|:-------------------|:-------------------------|:---------------|:-----------------------|:-------------------------------|------|-----------------------| +// | ICLA (individual) |Hello %s,
-This is a notification email from EasyCLA regarding the project %s.
-The EasyCLA approval list for %s for project %s was modified.
-The modification was as follows:
-%s -%s -%s`, - recipientName, projectName, companyName, projectName, buildApprovalListSummary(approvalListChanges), - utils.GetEmailHelpContent(claGroupModel.Version == utils.V2), utils.GetEmailSignOffContent()) - - err := utils.SendEmail(subject, body, recipients) - if err != nil { - log.WithFields(f).Warnf("problem sending email with subject: %s to recipients: %+v, error: %+v", subject, recipients, err) - } else { - log.WithFields(f).Debugf("sent email with subject: %s to recipients: %+v", subject, recipients) + "functionName": "v1.signatures.service.updateChangeRequest", + "repositoryID": repositoryID, + "pullRequestID": pullRequestID, + "projectID": projectID, + } + + githubRepository, ghErr := github.GetGitHubRepository(ctx, ghOrg.OrganizationInstallationID, repositoryID) + if ghErr != nil { + log.WithFields(f).WithError(ghErr).Warn("unable to get github repository") + return ghErr + } + if githubRepository == nil || githubRepository.Owner == nil { + msg := "unable to get github repository - repository response is nil or owner is nil" + log.WithFields(f).Warn(msg) + return errors.New(msg) + } + // log.WithFields(f).Debugf("githubRepository: %+v", githubRepository) + if githubRepository.Name == nil || githubRepository.Owner.Login == nil { + msg := fmt.Sprintf("unable to get github repository - missing repository name or owner name for repository ID: %d", repositoryID) + log.WithFields(f).Warn(msg) + return errors.New(msg) + } + + gitHubOrgName := utils.StringValue(githubRepository.Owner.Login) + gitHubRepoName := utils.StringValue(githubRepository.Name) + + // Fetch committers + log.WithFields(f).Debugf("fetching commit authors for PR: %d using repository owner: %s, repo: %s", pullRequestID, gitHubOrgName, gitHubRepoName) + authors, latestSHA, authorsErr := github.GetPullRequestCommitAuthors(ctx, ghOrg.OrganizationInstallationID, int(pullRequestID), gitHubOrgName, gitHubRepoName) + if authorsErr != nil { + log.WithFields(f).WithError(authorsErr).Warnf("unable to get commit authors for %s/%s for PR: %d", gitHubOrgName, gitHubRepoName, pullRequestID) + return authorsErr + } + log.WithFields(f).Debugf("found %d commit authors for %s/%s for PR: %d", len(authors), gitHubOrgName, gitHubRepoName, pullRequestID) + + signed := make([]*github.UserCommitSummary, 0) + unsigned := make([]*github.UserCommitSummary, 0) + + // triage signed and unsigned users + log.WithFields(f).Debugf("triaging %d commit authors for PR: %d using repository %s/%s", + len(authors), pullRequestID, gitHubOrgName, gitHubRepoName) + for _, userSummary := range authors { + + if !userSummary.IsValid() { + log.WithFields(f).Debugf("invalid user summary: %+v", *userSummary) + unsigned = append(unsigned, userSummary) + continue + } + + commitAuthorID := userSummary.GetCommitAuthorID() + commitAuthorUsername := userSummary.GetCommitAuthorUsername() + commitAuthorEmail := userSummary.GetCommitAuthorEmail() + + log.WithFields(f).Debugf("checking user - sha: %s, user ID: %s, username: %s, email: %s", + userSummary.SHA, commitAuthorID, commitAuthorUsername, commitAuthorEmail) + + var user *models.User + var userErr error + + if commitAuthorID != "" { + log.WithFields(f).Debugf("looking up user by ID: %s", commitAuthorID) + user, userErr = s.usersService.GetUserByGitHubID(commitAuthorID) + if userErr != nil { + log.WithFields(f).WithError(userErr).Warnf("unable to get user by github id: %s", commitAuthorID) + } + if user != nil { + log.WithFields(f).Debugf("found user by ID: %s", commitAuthorID) + } + } + if user == nil && commitAuthorUsername != "" { + log.WithFields(f).Debugf("looking up user by username: %s", commitAuthorUsername) + user, userErr = s.usersService.GetUserByGitHubUsername(commitAuthorUsername) + if userErr != nil { + log.WithFields(f).WithError(userErr).Warnf("unable to get user by github username: %s", commitAuthorUsername) + } + if user != nil { + log.WithFields(f).Debugf("found user by username: %s", commitAuthorUsername) + } + } + if user == nil && commitAuthorEmail != "" { + log.WithFields(f).Debugf("looking up user by email: %s", commitAuthorEmail) + user, userErr = s.usersService.GetUserByEmail(commitAuthorEmail) + if userErr != nil { + log.WithFields(f).WithError(userErr).Warnf("unable to get user by user email: %s", commitAuthorEmail) + } + if user != nil { + log.WithFields(f).Debugf("found user by email: %s", commitAuthorEmail) + } + } + + if user == nil { + log.WithFields(f).Debugf("unable to find user for commit author - sha: %s, user ID: %s, username: %s, email: %s", + userSummary.SHA, commitAuthorID, commitAuthorUsername, commitAuthorEmail) + unsigned = append(unsigned, userSummary) + continue + } + + log.WithFields(f).Debugf("checking to see if user has signed an ICLA or ECLA for project: %s", projectID) + userSigned, companyAffiliation, signedErr := s.HasUserSigned(ctx, user, projectID) + if signedErr != nil { + log.WithFields(f).WithError(signedErr).Warnf("has user signed error - user: %+v, project: %s", user, projectID) + unsigned = append(unsigned, userSummary) + continue + } + + if companyAffiliation != nil { + userSummary.Affiliated = *companyAffiliation + } + + if userSigned != nil { + userSummary.Authorized = *userSigned + if userSummary.Authorized { + signed = append(signed, userSummary) + } else { + unsigned = append(unsigned, userSummary) + } + } } + + log.WithFields(f).Debugf("commit authors status => signed: %+v and missing: %+v", signed, unsigned) + + // update pull request + updateErr := github.UpdatePullRequest(ctx, ghOrg.OrganizationInstallationID, int(pullRequestID), gitHubOrgName, gitHubRepoName, githubRepository.ID, *latestSHA, signed, unsigned, s.claBaseAPIURL, s.claLandingPage, s.claLogoURL) + if updateErr != nil { + log.WithFields(f).Debugf("unable to update PR: %d", pullRequestID) + return updateErr + } + + return nil } -// getAddEmailContributors is a helper function to lookup the contributors impacted by the Approval List update -func (s service) getAddEmailContributors(approvalList *models.ApprovalList) []*models.User { - var userModelList []*models.User - for _, value := range approvalList.AddEmailApprovalList { - userModel, err := s.usersService.GetUserByEmail(value) +// hasUserSigned checks to see if the user has signed an ICLA or ECLA for the project, returns: +// false, false, nil if user is not authorized for ICLA or ECLA +// false, false, some error if user is not authorized for ICLA or ECLA - we has some problem looking up stuff +// true, false, nil if user has an ICLA (authorized, but not company affiliation, no error) +// true, true, nil if user has an ECLA (authorized, with company affiliation, no error) +func (s service) HasUserSigned(ctx context.Context, user *models.User, projectID string) (*bool, *bool, error) { + f := logrus.Fields{ + "functionName": "v1.signatures.service.updateChangeRequest", + "projectID": projectID, + "user": user, + } + var hasSigned bool + var companyAffiliation bool + + approved := true + signed := true + + // Check for ICLA + log.WithFields(f).Debugf("checking to see if user has signed an ICLA") + signature, sigErr := s.GetIndividualSignature(ctx, projectID, user.UserID, &approved, &signed) + if sigErr != nil { + log.WithFields(f).WithError(sigErr).Warnf("problem checking for ICLA signature for user: %s", user.UserID) + return &hasSigned, &companyAffiliation, sigErr + } + if signature != nil { + hasSigned = true + log.WithFields(f).Debugf("ICLA signature check passed for user: %+v on project : %s", user, projectID) + return &hasSigned, &companyAffiliation, nil // ICLA passes, no company affiliation + } else { + log.WithFields(f).Debugf("ICLA signature check failed for user: %+v on project: %s - ICLA not signed", user, projectID) + } + + // Check for Employee Acknowledgment ECLA + companyID := user.CompanyID + log.WithFields(f).Debugf("checking to see if user has signed a ECLA for company: %s", companyID) + + if companyID != "" { + companyAffiliation = true + + // Get employee signature + log.WithFields(f).Debugf("ECLA signature check - user has a company: %s - looking for user's employee acknowledgement...", companyID) + + // Load the company - make sure it is valid + companyModel, compModelErr := s.companyService.GetCompany(ctx, companyID) + if compModelErr != nil { + log.WithFields(f).WithError(compModelErr).Warnf("problem looking up company: %s", companyID) + return &hasSigned, &companyAffiliation, compModelErr + } + + // Load the CLA Group - make sure it is valid + claGroupModel, claGroupModelErr := s.claGroupService.GetCLAGroupByID(ctx, projectID) + if claGroupModelErr != nil { + log.WithFields(f).WithError(claGroupModelErr).Warnf("problem looking up project: %s", projectID) + return &hasSigned, &companyAffiliation, claGroupModelErr + } + + employeeSigned, err := s.ProcessEmployeeSignature(ctx, companyModel, claGroupModel, user) + if err != nil { - log.Warnf("unable to lookup user by LF email: %s, error: %+v", value, err) - } else { - userModelList = append(userModelList, userModel) + log.WithFields(f).WithError(err).Warnf("problem looking up employee signature for company: %s", companyID) + return &hasSigned, &companyAffiliation, err + } + if employeeSigned != nil { + hasSigned = *employeeSigned } + + } else { + log.WithFields(f).Debugf("ECLA signature check - user does not have a company ID assigned - skipping...") } - return userModelList + return &hasSigned, &companyAffiliation, nil } -// getRemoveEmailContributors is a helper function to lookup the contributors impacted by the Approval List update -func (s service) getRemoveEmailContributors(approvalList *models.ApprovalList) []*models.User { - var userModelList []*models.User - for _, value := range approvalList.RemoveEmailApprovalList { - userModel, err := s.usersService.GetUserByEmail(value) - if err != nil { - log.Warnf("unable to lookup user by LF email: %s, error: %+v", value, err) - } else { - userModelList = append(userModelList, userModel) +func (s service) ProcessEmployeeSignature(ctx context.Context, companyModel *models.Company, claGroupModel *models.ClaGroup, user *models.User) (*bool, error) { + f := logrus.Fields{ + "functionName": "v2.signatures.service.ProcessEmployeeSignature", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companyID": companyModel.CompanyID, + "projectID": claGroupModel.ProjectID, + "userID": user.UserID, + } + var wg sync.WaitGroup + resultChannel := make(chan *EmployeeModel) + errorChannel := make(chan error) + hasSigned := false + projectID := claGroupModel.ProjectID + companyID := companyModel.CompanyID + approved := true + signed := true + + wg.Add(1) + go s.repo.GetProjectCompanyEmployeeSignature(ctx, companyModel, claGroupModel, user, &wg, resultChannel, errorChannel) + + go func() { + wg.Wait() + close(resultChannel) + close(errorChannel) + }() + + for result := range resultChannel { + if result != nil { + employeeSignature := result.Signature + if employeeSignature != nil { + // log.WithFields(f).Debugf("ECLA Signature check - located employee acknowledgement - signature id: %s", employeeSignature.SignatureID) + log.WithFields(f).Debugf("ecla signature check - :%+v", employeeSignature) + + // Get corporate ccla signature of company to access the approval list + cclaSignature, cclaErr := s.GetCorporateSignature(ctx, projectID, companyID, &approved, &signed) + if cclaErr != nil { + log.WithFields(f).WithError(cclaErr).Warnf("problem looking up ECLA signature for company: %s, project: %s", companyID, projectID) + return &hasSigned, cclaErr + } + + if cclaSignature != nil { + log.WithFields(f).Debug("found ccla signature") + userApproved, approvedErr := s.UserIsApproved(ctx, user, cclaSignature) + if approvedErr != nil { + log.WithFields(f).WithError(approvedErr).Warnf("problem determining if user: %s is approved for project: %s", user.UserID, projectID) + return &hasSigned, approvedErr + } + log.WithFields(f).Debugf("ECLA Signature check - user approved: %t for projectID: %s for company: %s", userApproved, projectID, user.CompanyID) + + if userApproved { + log.WithFields(f).Debugf("user: %s is in the approval list for signature: %s", user.UserID, employeeSignature.SignatureID) + hasSigned = true + } else { + log.WithFields(f).Debugf("user: %s is not in the approval list for signature: %s", user.UserID, employeeSignature.SignatureID) + } + } + } else { + log.WithFields(f).Debugf("ECLA Signature check - unable to locate employee acknowledgement for user: %s, company: %s, project: %s", user.UserID, companyID, projectID) + } } } - return userModelList + for empSigErr := range errorChannel { + log.WithFields(f).WithError(empSigErr).Warnf("problem looking up employee signature for user: %s, company: %s, project: %s", user.UserID, companyID, projectID) + return &hasSigned, empSigErr + } + + return &hasSigned, nil + } -// getAddGitHubContributors is a helper function to lookup the contributors impacted by the Approval List update -func (s service) getAddGitHubContributors(approvalList *models.ApprovalList) []*models.User { - var userModelList []*models.User - for _, value := range approvalList.AddGithubUsernameApprovalList { - userModel, err := s.usersService.GetUserByGitHubUsername(value) +func (s service) UserIsApproved(ctx context.Context, user *models.User, cclaSignature *models.Signature) (bool, error) { + // add lf email to emails + f := logrus.Fields{ + "functionName": "v1.signatures.service.UserIsApproved", + } + + emails := user.Emails + + if user.LfEmail != "" { + log.WithFields(f).Debugf("adding lf email: %s to emails", user.LfEmail) + emails = append(emails, string(user.LfEmail)) + // remove duplicates + log.WithFields(f).Debug("removing duplicates") + emails = utils.RemoveDuplicates(emails) + } + + // check GitHub username approval list + log.WithFields(f).Debug("checking if user is in the approval list") + gitHubUsernameApprovalList := cclaSignature.GithubUsernameApprovalList + if len(gitHubUsernameApprovalList) > 0 { + for _, gitHubUsername := range gitHubUsernameApprovalList { + if strings.EqualFold(gitHubUsername, strings.TrimSpace(user.GithubUsername)) { + return true, nil + } + } + } else { + log.WithFields(f).Debugf("no matching github username found in ccla: %s", cclaSignature.SignatureID) + } + + // check GitLab username approval list + gitLabUsernameApprovalList := cclaSignature.GitlabUsernameApprovalList + if len(gitLabUsernameApprovalList) > 0 { + for _, gitLabUsername := range gitLabUsernameApprovalList { + if strings.EqualFold(gitLabUsername, strings.TrimSpace(user.GitlabUsername)) { + return true, nil + } + } + } else { + log.WithFields(f).Debugf("no matching gitlab username found in ccla: %s", cclaSignature.SignatureID) + } + + // check email email approval list + emailApprovalList := cclaSignature.EmailApprovalList + log.WithFields(f).Debugf("checking if user is in the email approval list: %+v with emails :%v", emailApprovalList, emails) + if len(emailApprovalList) > 0 { + for _, email := range emails { + log.WithFields(f).Debugf("checking email: %s", email) + if utils.StringInSlice(email, emailApprovalList) { + log.WithFields(f).Debugf("found matching email: %s in the email approval list", email) + return true, nil + } + } + } else { + log.WithFields(f).Debugf("no matching email found in ccla: %s", cclaSignature.SignatureID) + } + + // check domain email approval list + domainApprovalList := cclaSignature.DomainApprovalList + if len(domainApprovalList) > 0 { + matched, err := s.processPattern(emails, domainApprovalList) if err != nil { - log.Warnf("unable to lookup user by GitHub username: %s, error: %+v", value, err) - } else { - userModelList = append(userModelList, userModel) + return false, err + } + if matched != nil && *matched { + return true, nil + } + } + + // check github org email ApprovalList + if user.GithubUsername != "" { + githubOrgApprovalList := cclaSignature.GithubOrgApprovalList + if len(githubOrgApprovalList) > 0 { + log.WithFields(f).Debugf("determining if github user :%s is associated with ant of the github orgs : %+v", user.GithubUsername, githubOrgApprovalList) + } + + for _, org := range githubOrgApprovalList { + membership, err := github.GetMembership(ctx, user.GithubUsername, org) + if err != nil { + break + } + if membership != nil { + log.WithFields(f).Debugf("found matching github organization: %s for user: %s", org, user.GithubUsername) + return true, nil + } else { + log.WithFields(f).Debugf("user: %s is not in the organization: %s", user.GithubUsername, org) + } } } - return userModelList + return false, nil } -// getRemoveGitHubContributors is a helper function to lookup the contributors impacted by the Approval List update -func (s service) getRemoveGitHubContributors(approvalList *models.ApprovalList) []*models.User { - var userModelList []*models.User - for _, value := range approvalList.RemoveGithubUsernameApprovalList { - userModel, err := s.usersService.GetUserByGitHubUsername(value) +func (s service) processPattern(emails []string, patterns []string) (*bool, error) { + matched := false + + for _, pattern := range patterns { + if strings.HasPrefix(pattern, "*.") { + pattern = strings.Replace(pattern, "*.", ".*", -1) + } else if strings.HasPrefix(pattern, "*") { + pattern = strings.Replace(pattern, "*", ".*", -1) + } else if strings.HasPrefix(pattern, ".") { + pattern = strings.Replace(pattern, ".", ".*", -1) + } + + preProcessedPattern := fmt.Sprintf("^.*@%s$", pattern) + compiled, err := regexp.Compile(preProcessedPattern) if err != nil { - log.Warnf("unable to lookup user by GitHub username: %s, error: %+v", value, err) - } else { - userModelList = append(userModelList, userModel) - } - } - - return userModelList -} -func (s service) sendRequestAccessEmailToContributors(authUser *auth.User, companyModel *models.Company, claGroupModel *models.ClaGroup, approvalList *models.ApprovalList) { - addEmailUsers := s.getAddEmailContributors(approvalList) - for _, user := range addEmailUsers { - sendRequestAccessEmailToContributorRecipient(authUser, companyModel, claGroupModel, user.Username, user.LfEmail, "added", "to", "you are authorized to contribute to") - } - removeEmailUsers := s.getRemoveEmailContributors(approvalList) - for _, user := range removeEmailUsers { - sendRequestAccessEmailToContributorRecipient(authUser, companyModel, claGroupModel, user.Username, user.LfEmail, "removed", "from", "you are no longer authorized to contribute to") - } - addGitHubUsers := s.getAddGitHubContributors(approvalList) - for _, user := range addGitHubUsers { - sendRequestAccessEmailToContributorRecipient(authUser, companyModel, claGroupModel, user.Username, user.LfEmail, "added", "to", "you are authorized to contribute to") - } - removeGitHubUsers := s.getRemoveGitHubContributors(approvalList) - for _, user := range removeGitHubUsers { - sendRequestAccessEmailToContributorRecipient(authUser, companyModel, claGroupModel, user.Username, user.LfEmail, "removed", "from", "you are no longer authorized to contribute to") - } -} - -func (s service) createEventLogEntries(companyModel *models.Company, claGroupModel *models.ClaGroup, userModel *models.User, approvalList *models.ApprovalList) { - for _, value := range approvalList.AddEmailApprovalList { - // Send an event - s.eventsService.LogEvent(&events.LogEventArgs{ - EventType: events.ClaApprovalListUpdated, - ProjectID: claGroupModel.ProjectID, - ClaGroupModel: claGroupModel, - CompanyID: companyModel.CompanyID, - CompanyModel: companyModel, - LfUsername: userModel.LfUsername, - UserID: userModel.UserID, - UserModel: userModel, - ExternalProjectID: claGroupModel.ProjectExternalID, - EventData: &events.CLAApprovalListAddEmailData{ - UserName: userModel.LfUsername, - UserEmail: userModel.LfEmail, - UserLFID: userModel.UserID, - ApprovalListEmail: value, - }, - }) - } - for _, value := range approvalList.RemoveEmailApprovalList { - // Send an event - s.eventsService.LogEvent(&events.LogEventArgs{ - EventType: events.ClaApprovalListUpdated, - ProjectID: claGroupModel.ProjectID, - ClaGroupModel: claGroupModel, - CompanyID: companyModel.CompanyID, - CompanyModel: companyModel, - LfUsername: userModel.LfUsername, - UserID: userModel.UserID, - UserModel: userModel, - ExternalProjectID: claGroupModel.ProjectExternalID, - EventData: &events.CLAApprovalListRemoveEmailData{ - UserName: userModel.LfUsername, - UserEmail: userModel.LfEmail, - UserLFID: userModel.UserID, - ApprovalListEmail: value, - }, - }) + return nil, err + } + + for _, email := range emails { + if compiled.MatchString(email) { + matched = true + break + } + } } - for _, value := range approvalList.AddDomainApprovalList { - // Send an event - s.eventsService.LogEvent(&events.LogEventArgs{ - EventType: events.ClaApprovalListUpdated, - ProjectID: claGroupModel.ProjectID, - ClaGroupModel: claGroupModel, - CompanyID: companyModel.CompanyID, - CompanyModel: companyModel, - LfUsername: userModel.LfUsername, - UserID: userModel.UserID, - UserModel: userModel, - ExternalProjectID: claGroupModel.ProjectExternalID, - EventData: &events.CLAApprovalListAddDomainData{ - UserName: userModel.LfUsername, - UserEmail: userModel.LfEmail, - UserLFID: userModel.UserID, - ApprovalListDomain: value, - }, - }) + + return &matched, nil +} + +func (s service) handleGitHubStatusUpdate(ctx context.Context, employeeUserModel *models.User) error { + if employeeUserModel == nil { + return fmt.Errorf("employee user model is nil") } - for _, value := range approvalList.RemoveDomainApprovalList { - // Send an event - s.eventsService.LogEvent(&events.LogEventArgs{ - EventType: events.ClaApprovalListUpdated, - ProjectID: claGroupModel.ProjectID, - ClaGroupModel: claGroupModel, - CompanyID: companyModel.CompanyID, - CompanyModel: companyModel, - LfUsername: userModel.LfUsername, - UserID: userModel.UserID, - UserModel: userModel, - ExternalProjectID: claGroupModel.ProjectExternalID, - EventData: &events.CLAApprovalListRemoveDomainData{ - UserName: userModel.LfUsername, - UserEmail: userModel.LfEmail, - UserLFID: userModel.UserID, - ApprovalListDomain: value, - }, - }) + + f := logrus.Fields{ + "functionName": "v1.signatures.service.handleGitHubStatusUpdate", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "userID": employeeUserModel.UserID, + "gitHubUsername": employeeUserModel.GithubUsername, + "gitHubID": employeeUserModel.GithubID, + "userEmail": employeeUserModel.LfEmail.String(), } - for _, value := range approvalList.AddGithubUsernameApprovalList { - // Send an event - s.eventsService.LogEvent(&events.LogEventArgs{ - EventType: events.ClaApprovalListUpdated, - ProjectID: claGroupModel.ProjectID, - ClaGroupModel: claGroupModel, - CompanyID: companyModel.CompanyID, - CompanyModel: companyModel, - LfUsername: userModel.LfUsername, - UserID: userModel.UserID, - UserModel: userModel, - ExternalProjectID: claGroupModel.ProjectExternalID, - EventData: &events.CLAApprovalListAddGitHubUsernameData{ - UserName: userModel.LfUsername, - UserEmail: userModel.LfEmail, - UserLFID: userModel.UserID, - ApprovalListGitHubUsername: value, - }, - }) + + log.WithFields(f).Debugf("processing GitHub status check request for user: %s", employeeUserModel.GitlabUsername) + signatureMetadata, activeSigErr := s.repo.GetActivePullRequestMetadata(ctx, employeeUserModel.GithubUsername, employeeUserModel.LfEmail.String()) + if activeSigErr != nil { + log.WithFields(f).WithError(activeSigErr).Warnf("unable to get active pull request metadata for user: %+v - unable to update GitHub status", employeeUserModel) + return activeSigErr } - for _, value := range approvalList.RemoveGithubUsernameApprovalList { - // Send an event - s.eventsService.LogEvent(&events.LogEventArgs{ - EventType: events.ClaApprovalListUpdated, - ProjectID: claGroupModel.ProjectID, - ClaGroupModel: claGroupModel, - CompanyID: companyModel.CompanyID, - CompanyModel: companyModel, - LfUsername: userModel.LfUsername, - UserID: userModel.UserID, - UserModel: userModel, - ExternalProjectID: claGroupModel.ProjectExternalID, - EventData: &events.CLAApprovalListRemoveGitHubUsernameData{ - UserName: userModel.LfUsername, - UserEmail: userModel.LfEmail, - UserLFID: userModel.UserID, - ApprovalListGitHubUsername: value, - }, - }) + if signatureMetadata == nil { + log.WithFields(f).Debugf("unable to get active pull requst metadata for user: %+v - unable to update GitHub status", employeeUserModel) + return nil } - for _, value := range approvalList.AddGithubOrgApprovalList { - // Send an event - s.eventsService.LogEvent(&events.LogEventArgs{ - EventType: events.ClaApprovalListUpdated, - ProjectID: claGroupModel.ProjectID, - ClaGroupModel: claGroupModel, - CompanyID: companyModel.CompanyID, - CompanyModel: companyModel, - LfUsername: userModel.LfUsername, - UserID: userModel.UserID, - UserModel: userModel, - ExternalProjectID: claGroupModel.ProjectExternalID, - EventData: &events.CLAApprovalListAddGitHubOrgData{ - UserName: userModel.LfUsername, - UserEmail: userModel.LfEmail, - UserLFID: userModel.UserID, - ApprovalListGitHubOrg: value, - }, - }) + + // Fetch easycla repository + claRepository, repoErr := s.repositoryService.GetRepositoryByExternalID(ctx, signatureMetadata.RepositoryID) + if repoErr != nil { + log.WithFields(f).WithError(repoErr).Warnf("unable to fetch repository by ID: %s - unable to update GitHub status", signatureMetadata.RepositoryID) + return repoErr } - for _, value := range approvalList.RemoveGithubOrgApprovalList { - // Send an event - s.eventsService.LogEvent(&events.LogEventArgs{ - EventType: events.ClaApprovalListUpdated, - ProjectID: claGroupModel.ProjectID, - ClaGroupModel: claGroupModel, - CompanyID: companyModel.CompanyID, - CompanyModel: companyModel, - LfUsername: userModel.LfUsername, - UserID: userModel.UserID, - UserModel: userModel, - ExternalProjectID: claGroupModel.ProjectExternalID, - EventData: &events.CLAApprovalListRemoveGitHubOrgData{ - UserName: userModel.LfUsername, - UserEmail: userModel.LfEmail, - UserLFID: userModel.UserID, - ApprovalListGitHubOrg: value, - }, - }) + + if !claRepository.Enabled { + log.WithFields(f).Debugf("repository: %s associated with PR: %s is NOT enabled - unable to update GitHub status", claRepository.RepositoryURL, signatureMetadata.PullRequestID) + return nil } -} -func (s service) GetClaGroupICLASignatures(ctx context.Context, claGroupID string, searchTerm *string) (*models.IclaSignatures, error) { - return s.repo.GetClaGroupICLASignatures(ctx, claGroupID, searchTerm) -} + // fetch GitHub org details + githubOrg, githubOrgErr := s.githubOrgService.GetGitHubOrganizationByName(ctx, claRepository.RepositoryOrganizationName) + if githubOrgErr != nil { + log.WithFields(f).WithError(githubOrgErr).Warnf("unable to lookup GitHub organization by name: %s - unable to update GitHub status", claRepository.RepositoryOrganizationName) + return githubOrgErr + } -func (s service) GetClaGroupCCLASignatures(ctx context.Context, claGroupID string) (*models.Signatures, error) { - return s.repo.GetProjectSignatures(ctx, signatures.GetProjectSignaturesParams{ - ClaType: aws.String(utils.ClaTypeCCLA), - ProjectID: claGroupID, - }, 1000) -} - -func (s service) GetClaGroupCorporateContributors(ctx context.Context, claGroupID string, companyID *string, searchTerm *string) (*models.CorporateContributorList, error) { - return s.repo.GetClaGroupCorporateContributors(ctx, claGroupID, companyID, searchTerm) -} - -// sendRequestAccessEmailToContributors sends the request access email to the specified contributors -func sendRequestAccessEmailToContributorRecipient(authUser *auth.User, companyModel *models.Company, claGroupModel *models.ClaGroup, recipientName, recipientAddress, addRemove, toFrom, authorizedString string) { - companyName := companyModel.CompanyName - projectName := claGroupModel.ProjectName - - // subject string, body string, recipients []string - subject := fmt.Sprintf("EasyCLA: Approval List Update for %s on %s", companyName, projectName) - recipients := []string{recipientAddress} - body := fmt.Sprintf(` -Hello %s,
-This is a notification email from EasyCLA regarding the project %s.
-You have been %s %s the Approval List of %s for %s by CLA Manager %s. This means that %s on behalf of %s.
-If you had previously submitted one or more pull requests to %s that had failed, you should -close and re-open the pull request to force a recheck by the EasyCLA system.
-%s -%s`, - recipientName, projectName, addRemove, toFrom, - companyName, projectName, authUser.UserName, authorizedString, projectName, projectName, - utils.GetEmailHelpContent(claGroupModel.Version == utils.V2), utils.GetEmailSignOffContent()) - - err := utils.SendEmail(subject, body, recipients) - if err != nil { - log.Warnf("problem sending email with subject: %s to recipients: %+v, error: %+v", subject, recipients, err) - } else { - log.Debugf("sent email with subject: %s to recipients: %+v", subject, recipients) + repositoryID, idErr := strconv.Atoi(signatureMetadata.RepositoryID) + if idErr != nil { + log.WithFields(f).WithError(idErr).Warnf("unable to convert repository ID: %s to integer - unable to update GitHub status", signatureMetadata.RepositoryID) + return idErr } -} -// getBestEmail is a helper function to return the best email address for the user model -func getBestEmail(claManager models.User) string { - if claManager.LfEmail != "" { - return claManager.LfEmail + pullRequestID, idErr := strconv.Atoi(signatureMetadata.PullRequestID) + if idErr != nil { + log.WithFields(f).WithError(idErr).Warnf("unable to convert pull request ID: %s to integer - unable to update GitHub status", signatureMetadata.RepositoryID) + return idErr } - for _, email := range claManager.Emails { - if email != "" { - return email - } + // Update change request + log.WithFields(f).Debugf("updating change request for repository: %d, pull request: %d", repositoryID, pullRequestID) + updateErr := s.updateChangeRequest(ctx, githubOrg, int64(repositoryID), int64(pullRequestID), signatureMetadata.CLAGroupID) + if updateErr != nil { + log.WithFields(f).WithError(updateErr).Warnf("unable to update pull request: %d", pullRequestID) + return updateErr } - return "" + return nil } diff --git a/cla-backend-go/signatures/service_test.go b/cla-backend-go/signatures/service_test.go new file mode 100644 index 000000000..9d35f9da2 --- /dev/null +++ b/cla-backend-go/signatures/service_test.go @@ -0,0 +1,85 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package signatures + +import ( + "context" + "testing" + + v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + "github.com/stretchr/testify/assert" +) + +func TestUserIsApproved(t *testing.T) { + ctx := context.Background() + + testCases := []struct { + name string + user *v1Models.User + cclaSignature *v1Models.Signature + expectedIsApproved bool + }{ + { + name: "User in GitHub username approval list", + user: &v1Models.User{ + GithubUsername: "approved-user", + }, + cclaSignature: &v1Models.Signature{ + GithubUsernameApprovalList: []string{"approved-user"}, + }, + expectedIsApproved: true, + }, + { + name: "User not in GitHub username approval list", + user: &v1Models.User{ + GithubUsername: "unapproved-user", + }, + cclaSignature: &v1Models.Signature{ + GithubUsernameApprovalList: []string{"approved-user"}, + }, + expectedIsApproved: false, + }, + { + name: "User in Email approval list", + user: &v1Models.User{ + Emails: []string{"foo@gmail.com"}, + }, + cclaSignature: &v1Models.Signature{ + EmailApprovalList: []string{"foo@gmail.com"}, + }, + expectedIsApproved: true, + }, + { + name: "User not in Email approval list", + user: &v1Models.User{ + Emails: []string{"unapproved@gmail.com"}, + }, + cclaSignature: &v1Models.Signature{ + EmailApprovalList: []string{"approved@gmail.com"}, + }, + expectedIsApproved: false, + }, + { + name: "User in Domain approval list", + user: &v1Models.User{ + Emails: []string{"approved@samsung.com"}, + }, + cclaSignature: &v1Models.Signature{ + DomainApprovalList: []string{"samsung.com"}, + }, + expectedIsApproved: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + service := NewService(nil, nil, nil, nil, false, nil, nil, nil, nil, "", "", "") + + isApproved, err := service.UserIsApproved(ctx, tc.user, tc.cclaSignature) + + assert.Nil(t, err) + assert.Equal(t, tc.expectedIsApproved, isApproved) + }) + } +} diff --git a/cla-backend-go/swagger/cla.v1.yaml b/cla-backend-go/swagger/cla.v1.yaml index 0a5b85134..15656711f 100644 --- a/cla-backend-go/swagger/cla.v1.yaml +++ b/cla-backend-go/swagger/cla.v1.yaml @@ -65,16 +65,14 @@ paths: description: The unique request ID value - assigned/set by the API Gateway based on the session schema: $ref: '#/definitions/health' - '503': - description: '' - schema: - $ref: '#/definitions/health' '400': $ref: '#/responses/invalid-request' '401': $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '503': + $ref: '#/responses/service-unavailable' tags: - health @@ -169,6 +167,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' tags: - users delete: @@ -200,6 +200,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' tags: - users @@ -350,6 +352,8 @@ paths: - $ref: '#/parameters/signatureType' - $ref: '#/parameters/claType' - $ref: '#/parameters/sortOrder' + - $ref: '#/parameters/approved' + - $ref: '#/parameters/signed' responses: '200': description: 'Success' @@ -365,6 +369,53 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' + tags: + - signatures + + /signatures/project/{projectID}/summary-report: + post: + summary: Creates a summary report + description: Creates a summary report when provided the project ID + security: + - OauthSecurity: [ ] + operationId: createProjectSummaryReport + parameters: + - $ref: "#/parameters/x-request-id" + - $ref: "#/parameters/path-projectID" + - $ref: '#/parameters/pageSize' + - $ref: '#/parameters/nextKey' + - $ref: '#/parameters/searchTerm' + - $ref: '#/parameters/searchField' + - $ref: '#/parameters/fullMatch' + - $ref: '#/parameters/signatureType' + - $ref: '#/parameters/claType' + - $ref: '#/parameters/sortOrder' + - $ref: '#/parameters/approved' + - $ref: '#/parameters/signed' + - name: body + in: body + schema: + $ref: '#/definitions/company-id-list' + required: false + 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/signature-report' + '400': + $ref: '#/responses/invalid-request' + '401': + $ref: '#/responses/unauthorized' + '403': + $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' tags: - signatures @@ -381,6 +432,7 @@ paths: - $ref: '#/parameters/pageSize' - $ref: '#/parameters/nextKey' - $ref: '#/parameters/sortOrder' + - $ref: '#/parameters/searchTerm' responses: '200': description: 'Success' @@ -396,6 +448,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' tags: - signatures @@ -429,6 +483,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' tags: - signatures @@ -460,6 +516,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' tags: - signatures @@ -477,6 +535,7 @@ paths: - $ref: '#/parameters/pageSize' - $ref: '#/parameters/nextKey' - $ref: '#/parameters/sortOrder' + - $ref: '#/parameters/searchTerm' responses: '200': description: 'Success' @@ -492,6 +551,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' tags: - signatures @@ -564,6 +625,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' tags: - signatures delete: @@ -598,6 +661,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' tags: - signatures post: @@ -632,6 +697,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' tags: - signatures @@ -1140,6 +1207,7 @@ paths: parameters: - $ref: "#/parameters/x-request-id" - $ref: '#/parameters/companyName' + - $ref: '#/parameters/include-signing-entity-name' - name: websiteName in: query type: string @@ -1198,6 +1266,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' tags: - company @@ -1261,6 +1331,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' tags: - company @@ -1291,6 +1363,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' tags: - company @@ -1321,6 +1395,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' tags: - company @@ -1351,6 +1427,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' tags: - company @@ -1589,6 +1667,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' '409': $ref: '#/responses/conflict' tags: @@ -1625,6 +1705,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' '409': $ref: '#/responses/conflict' tags: @@ -1694,6 +1776,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' '409': $ref: '#/responses/conflict' tags: @@ -1795,6 +1879,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' '409': $ref: '#/responses/conflict' tags: @@ -1984,7 +2070,7 @@ paths: - in: body name: body schema: - $ref: '#/definitions/create-github-organization' + $ref: '#/definitions/github-create-organization' required: true responses: 200: @@ -2093,6 +2179,8 @@ paths: $ref: '#/responses/unauthorized' 403: $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' tags: - github-repositories get: @@ -2112,13 +2200,15 @@ paths: type: string description: The unique request ID value - assigned/set by the API Gateway based on the session schema: - $ref: '#/definitions/list-github-repositories' + $ref: '#/definitions/github-list-repositories' 400: $ref: '#/responses/invalid-request' 401: $ref: '#/responses/unauthorized' 403: $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' tags: - github-repositories /project/{projectSFID}/github/repositories/{repositoryID}: @@ -2210,6 +2300,7 @@ paths: - $ref: '#/parameters/userID' - $ref: '#/parameters/companyID' - $ref: '#/parameters/projectID' + - $ref: '#/parameters/projectSFID' - $ref: '#/parameters/before' - $ref: '#/parameters/after' - $ref: '#/parameters/userName' @@ -2259,10 +2350,10 @@ paths: $ref: '#/definitions/gerrit-repo-list' '400': $ref: '#/responses/invalid-request' - '404': - $ref: '#/responses/not-found' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' '500': $ref: '#/responses/internal-server-error' tags: @@ -2293,6 +2384,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' tags: - gerrits @@ -2327,6 +2420,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' '409': $ref: '#/responses/conflict' tags: @@ -2360,7 +2455,7 @@ parameters: type: string required: false x-omitempty: false - pattern: '^([\w\d\s\-\,\./]+){2,255}$' + pattern: '[^<>]*' minLength: 2 maxLength: 255 companyNameRequired: @@ -2423,6 +2518,18 @@ parameters: required: false # UUID v4 regex # pattern: '[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}' + approved: + name: approved + description: The signature approved query parameter. If set with a value of true, the query would return approved signatures. If set with a value of false, the query would return invalidated/disabled signatures. + in: query + type: boolean + required: false + signed: + name: signed + description: The signature signed query parameter. If set with a value of true, the query would return signed signatures. If set with a value of false, the query would return incomplete/unsigned signatures. + in: query + type: boolean + required: false sortOrder: name: sortOrder description: The sort order - either asc or desc @@ -2445,6 +2552,11 @@ parameters: description: unique id of the project in: query type: string + projectSFID: + name: projectSFID + description: unique id of the SF project + in: query + type: string companyID: name: company_id description: unique id of the company @@ -2551,6 +2663,13 @@ parameters: those are the available fields to use + include-signing-entity-name: + name: include-signing-entity-name + in: query + type: boolean + default: false + required: false + definitions: version: $ref: './common/version.yaml' @@ -2623,6 +2742,12 @@ definitions: github-org: $ref: './common/github-org.yaml' + company-id-list: + type: array + description: A list of company internal IDs + items: + type: string + company-invite-user: type: object x-nullable: false @@ -2735,12 +2860,15 @@ definitions: cla-group-document: $ref: './common/cla-group-document.yaml' + + document-tab: + $ref: './common/document-tab.yaml' create-cla-group-template: $ref: './common/create-cla-group-template.yaml' update-github-organization: - $ref: './common/update-github-organization.yaml' + $ref: './common/github-organization-update.yaml' template-pdfs: $ref: './common/template-pdfs.yaml' @@ -2830,6 +2958,10 @@ definitions: $ref: './common/signatures.yaml' signature: $ref: './common/signature.yaml' + signature-report: + $ref: './common/signature-report.yaml' + signature-summary: + $ref: './common/signature-summary.yaml' approval-list: $ref: './common/signature-approval-list.yaml' @@ -2982,8 +3114,8 @@ definitions: github-organizations: $ref: './common/github-organizations.yaml' - create-github-organization: - $ref: './common/create-github-organization.yaml' + github-create-organization: + $ref: './common/github-organization-create.yaml' github-organization: $ref: './common/github-organization.yaml' @@ -2991,8 +3123,8 @@ definitions: github-repository-info: $ref: './common/github-repository-info.yaml' - list-github-repositories: - $ref: './common/list-github-repositories.yaml' + github-list-repositories: + $ref: './common/github-repositories-list.yaml' org-list: $ref: './common/org-list.yaml' @@ -3057,6 +3189,14 @@ responses: description: The unique request ID value - assigned/set by the API Gateway based on the session schema: $ref: '#/definitions/error-response' + service-unavailable: + description: 'Service unavailable' + 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/error-response' conflict: description: The request could not be completed due to a conflict with the current state of the target resource. headers: diff --git a/cla-backend-go/swagger/cla.v2.yaml b/cla-backend-go/swagger/cla.v2.yaml index def1edb62..f84d6ee3a 100644 --- a/cla-backend-go/swagger/cla.v2.yaml +++ b/cla-backend-go/swagger/cla.v2.yaml @@ -57,16 +57,14 @@ paths: description: The unique request ID value - assigned/set by the API Gateway based on the session schema: $ref: '#/definitions/health' - '503': - description: 'Service unavailable' - schema: - $ref: '#/definitions/health' '400': $ref: '#/responses/invalid-request' '401': $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '503': + $ref: '#/responses/service-unavailable' tags: - health @@ -164,11 +162,7 @@ paths: - $ref: "#/parameters/x-acl" - $ref: "#/parameters/x-username" - $ref: "#/parameters/x-email" - - name: companyID - description: the company ID - in: path - type: string - required: true + - $ref: '#/parameters/path-companyID' responses: '200': description: 'Success' @@ -312,7 +306,7 @@ paths: tags: - metrics - /metrics/company/{companySFID}/project/{projectSFID}: + /metrics/company/{companyID}/project/{projectSFID}: get: summary: List project metrics for a company description: Returns list of project metrics for company @@ -322,7 +316,7 @@ paths: - $ref: "#/parameters/x-acl" - $ref: "#/parameters/x-username" - $ref: "#/parameters/x-email" - - $ref: "#/parameters/path-companySFID" + - $ref: "#/parameters/path-companyID" - $ref: "#/parameters/path-projectSFID" responses: '200': @@ -415,6 +409,8 @@ paths: $ref: '#/responses/forbidden' '404': $ref: '#/responses/not-found' + '409': + $ref: '#/responses/conflict' '500': $ref: '#/responses/internal-server-error' tags: @@ -963,8 +959,8 @@ paths: /events/foundation/{foundationSFID}: get: - summary: Get the events for the foundation - description: get all the events for the foundation + summary: Get foundation events + description: Returns events for the specified foundation operationId: getFoundationEvents parameters: - $ref: "#/parameters/path-foundationSFID" @@ -1000,8 +996,8 @@ paths: /events/project/{projectSFID}: get: - summary: Get the events for the project - description: get all the events for the project + summary: Get project events + description: Returns events for the specified project operationId: getProjectEvents parameters: - $ref: "#/parameters/path-projectSFID" @@ -1068,7 +1064,7 @@ paths: tags: - events - /company/{companySFID}/project/{projectSFID}/events: + /company/{companyID}/project/{projectSFID}/events: get: summary: Get recent events of company and project description: Returns list of events of company and project @@ -1080,8 +1076,9 @@ paths: - $ref: "#/parameters/x-email" - $ref: '#/parameters/pageSize' - $ref: '#/parameters/path-projectSFID' - - $ref: '#/parameters/path-companySFID' + - $ref: '#/parameters/path-companyID' - $ref: '#/parameters/nextKey' + - $ref: '#/parameters/searchTerm' - $ref: '#/parameters/returnAllEvents' produces: - application/json @@ -1257,7 +1254,9 @@ paths: tags: - template - + # --------------------------------------------------------------------------- + # GitHub Endpoint Definitions + # --------------------------------------------------------------------------- /project/{projectSFID}/github/organizations: post: summary: Add new GitHub Oranization in the project @@ -1275,7 +1274,7 @@ paths: - in: body name: body schema: - $ref: '#/definitions/create-github-organization' + $ref: '#/definitions/github-create-organization' required: true responses: '200': @@ -1292,6 +1291,8 @@ paths: $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '409': + $ref: '#/responses/conflict' '500': $ref: '#/responses/internal-server-error' tags: @@ -1352,7 +1353,7 @@ paths: - in: body name: body schema: - $ref: '#/definitions/update-github-organization' + $ref: '#/definitions/github-update-organization' required: true responses: '200': @@ -1435,13 +1436,17 @@ paths: type: string description: The unique request ID value - assigned/set by the API Gateway based on the session schema: - $ref: '#/definitions/github-repository' + $ref: '#/definitions/github-list-repositories' '400': $ref: '#/responses/invalid-request' '401': $ref: '#/responses/unauthorized' '403': $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' + '409': + $ref: '#/responses/conflict' '500': $ref: '#/responses/internal-server-error' tags: @@ -1467,7 +1472,7 @@ paths: type: string description: The unique request ID value - assigned/set by the API Gateway based on the session schema: - $ref: '#/definitions/list-github-repositories' + $ref: '#/definitions/github-list-repositories' '400': $ref: '#/responses/invalid-request' '401': @@ -1579,6 +1584,9 @@ paths: in: path type: string required: true + - name: branchName + in: query + type: string responses: '200': description: 'Success' @@ -1601,6 +1609,269 @@ paths: tags: - github-repositories + # --------------------------------------------------------------------------- + # GitLab Endpoint Definitions + # --------------------------------------------------------------------------- + /project/{projectSFID}/gitlab/organizations: + post: + summary: Add new Gitlab Organization in the project + description: Endpoint to create a new Gitlab Organization in EasyCLA + operationId: addProjectGitlabOrganization + parameters: + - $ref: "#/parameters/x-request-id" + - $ref: "#/parameters/x-acl" + - $ref: "#/parameters/x-username" + - $ref: "#/parameters/x-email" + - name: projectSFID + in: path + type: string + required: true + - in: body + name: body + schema: + $ref: '#/definitions/gitlab-create-organization' + 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/gitlab-project-organizations' + '400': + $ref: '#/responses/invalid-request' + '401': + $ref: '#/responses/unauthorized' + '403': + $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' + '409': + $ref: '#/responses/conflict' + '500': + $ref: '#/responses/internal-server-error' + tags: + - gitlab-organizations + get: + summary: Get the Gitlab organizations of the project + description: Endpoint to return the list of Gitlab organization for the project + operationId: getProjectGitlabOrganizations + parameters: + - $ref: "#/parameters/x-request-id" + - $ref: "#/parameters/x-acl" + - $ref: "#/parameters/x-username" + - $ref: "#/parameters/x-email" + - name: projectSFID + in: path + type: string + 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/gitlab-project-organizations' + '400': + $ref: '#/responses/invalid-request' + '401': + $ref: '#/responses/unauthorized' + '403': + $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' + '500': + $ref: '#/responses/internal-server-error' + tags: + - gitlab-organizations + + /project/{projectSFID}/gitlab/group/{gitLabGroupID}/config: + put: + summary: Update Gitlab Group/Organization Configuration + description: Endpoint to adjust the Gitlab Group/Organization Configuration by GitLab Group ID + operationId: updateProjectGitlabGroupConfig + parameters: + - $ref: "#/parameters/x-request-id" + - $ref: "#/parameters/x-acl" + - $ref: "#/parameters/x-username" + - $ref: "#/parameters/x-email" + - name: projectSFID + in: path + type: string + required: true + - name: gitLabGroupID + in: path + type: integer + required: true + - in: body + name: body + schema: + $ref: '#/definitions/gitlab-organization-update' + required: true + responses: + '200': + description: 'Resource Updated' + 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/gitlab-project-organizations' + '400': + $ref: '#/responses/invalid-request' + '401': + $ref: '#/responses/unauthorized' + '403': + $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' + tags: + - gitlab-organizations + + # /project/{projectSFID}/gitlab/organization?organization_full_path=linuxfoundation/product/test: + /project/{projectSFID}/gitlab/organization: + delete: + summary: Delete Gitlab Group/Organization Configuration + description: Endpoint to delete the Gitlab Group/Organization by Group ID + operationId: deleteProjectGitlabGroupConfig + parameters: + - $ref: "#/parameters/x-request-id" + - $ref: "#/parameters/x-acl" + - $ref: "#/parameters/x-username" + - $ref: "#/parameters/x-email" + - name: projectSFID + in: path + type: string + required: true + - name: organization_full_path + in: query + type: string + required: true + responses: + '204': + description: 'Deleted' + headers: + x-request-id: + type: string + description: The unique request ID value - assigned/set by the API Gateway based on the session + '400': + $ref: '#/responses/invalid-request' + '401': + $ref: '#/responses/unauthorized' + '403': + $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' + tags: + - gitlab-organizations + + /project/{projectSFID}/gitlab/repositories: + put: + summary: Enrolls/Unenrolls GitLab repositories for the CLA Group + description: Endpoint to enroll or unenroll GitLab repositories for the CLA Group + operationId: enrollGitLabRepository + parameters: + - $ref: "#/parameters/x-request-id" + - $ref: "#/parameters/x-acl" + - $ref: "#/parameters/x-username" + - $ref: "#/parameters/x-email" + - name: projectSFID + in: path + type: string + required: true + - in: body + name: gitlab-repositories-enroll + schema: + $ref: '#/definitions/gitlab-repositories-enroll' + 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/gitlab-repositories-list' + '400': + $ref: '#/responses/invalid-request' + '401': + $ref: '#/responses/unauthorized' + '403': + $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' + '500': + $ref: '#/responses/internal-server-error' + tags: + - gitlab-repositories + get: + summary: Get the GitLab repositories of the project + description: Endpoint to fetch the list of GitLab repositories for the project + operationId: getProjectGitLabRepositories + parameters: + - $ref: "#/parameters/x-request-id" + - $ref: "#/parameters/x-acl" + - $ref: "#/parameters/x-username" + - $ref: "#/parameters/x-email" + - name: projectSFID + in: path + type: string + 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/gitlab-repositories-list' + '400': + $ref: '#/responses/invalid-request' + '401': + $ref: '#/responses/unauthorized' + '403': + $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' + '500': + $ref: '#/responses/internal-server-error' + tags: + - gitlab-repositories + + /gitlab/group/{gitLabGroupID}/members: + get: + summary: List members of a given GitLab group + description: Endpoint that returs the list of GitLab organization members + operationId: getGitLabGroupMembers + security: [] + parameters: + - $ref: "#/parameters/x-request-id" + - name: gitLabGroupID + in: path + type: string + 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/gitlab-group-members-list' + '400': + $ref: '#/responses/invalid-request' + '404': + $ref: '#/responses/not-found' + tags: + - gitlab-organizations + /cla-group/{claGroupID}/icla/signatures: get: summary: List individual signatures for CLA Group @@ -1614,6 +1885,10 @@ paths: - $ref: "#/parameters/path-claGroupID" - $ref: '#/parameters/searchTerm' - $ref: '#/parameters/sortOrder' + - $ref: '#/parameters/pageSize' + - $ref: '#/parameters/nextKey' + - $ref: '#/parameters/approved' + - $ref: '#/parameters/signed' responses: '200': description: 'Success' @@ -1636,7 +1911,7 @@ paths: /cla-group/{claGroupID}/corporate-contributors: get: - summary: List corporate contributors for cla group + summary: List corporate contributors description: Returns a list of corporate contributor for the CLA Group operationId: listClaGroupCorporateContributors parameters: @@ -1645,8 +1920,10 @@ paths: - $ref: "#/parameters/x-username" - $ref: "#/parameters/x-email" - $ref: "#/parameters/path-claGroupID" - - $ref: "#/parameters/companySFID" + - $ref: "#/parameters/companyID" - $ref: '#/parameters/searchTerm' + - $ref: '#/parameters/pageSize' + - $ref: '#/parameters/nextKey' responses: '200': description: 'Success' @@ -1758,6 +2035,8 @@ paths: - $ref: '#/parameters/signatureType' - $ref: '#/parameters/claType' - $ref: '#/parameters/sortOrder' + - $ref: '#/parameters/signed' + - $ref: '#/parameters/approved' responses: '200': description: 'Success' @@ -1889,8 +2168,8 @@ paths: # -------------------------------------------------------- /signatures/project/{claGroupID}/ccla/pdfs: get: - summary: Downloads all corporate CLAs for this project - description: Downloads the corporate CLAs for this project + summary: Download corporate CLAs + description: Downloads all the corporate CLAs for this project operationId: downloadProjectSignatureCCLAs parameters: - $ref: "#/parameters/x-request-id" @@ -1991,7 +2270,7 @@ paths: # -------------------------------------------------------- # Employee CLA Endpoints - CSV Report Download # -------------------------------------------------------- - /signatures/project/{claGroupID}/company/{companySFID}/employee/csv: + /signatures/project/{claGroupID}/company/{companyID}/employee/csv: get: summary: Downloads all employee CLA information as a CSV document for this project description: Downloads the employee CLA information as a CSV document for this project @@ -2002,7 +2281,7 @@ paths: - $ref: "#/parameters/x-username" - $ref: "#/parameters/x-email" - $ref: "#/parameters/path-claGroupID" - - $ref: "#/parameters/path-companySFID" + - $ref: "#/parameters/path-companyID" produces: - text/json - text/csv @@ -2026,7 +2305,7 @@ paths: tags: - signatures - /signatures/project/{projectSFID}/company/{companySFID}: + /signatures/project/{projectSFID}/company/{companyID}: get: summary: Get project company ccla signatures description: Returns a list of ccla signature models when provided the project ID and company ID @@ -2037,7 +2316,7 @@ paths: - $ref: "#/parameters/x-username" - $ref: "#/parameters/x-email" - $ref: "#/parameters/path-projectSFID" - - $ref: "#/parameters/path-companySFID" + - $ref: "#/parameters/path-companyID" - $ref: '#/parameters/sortOrder' responses: '200': @@ -2047,7 +2326,7 @@ paths: type: string description: The unique request ID value - assigned/set by the API Gateway based on the session schema: - $ref: '#/definitions/signatures' + $ref: '#/definitions/corporate-signatures' '400': $ref: '#/responses/invalid-request' '401': @@ -2059,7 +2338,7 @@ paths: tags: - signatures - /signatures/company/{companySFID}: + /signatures/company/{companyID}: get: summary: Get company signatures description: Returns a list of company signatures when provided the company ID @@ -2069,7 +2348,7 @@ paths: - $ref: "#/parameters/x-acl" - $ref: "#/parameters/x-username" - $ref: "#/parameters/x-email" - - $ref: '#/parameters/path-companySFID' + - $ref: '#/parameters/path-companyID' - $ref: '#/parameters/signatureType' - $ref: '#/parameters/pageSize' - $ref: '#/parameters/nextKey' @@ -2134,7 +2413,7 @@ paths: tags: - signatures - /signatures/project/{projectSFID}/company/{companySFID}/employee: + /signatures/project/{projectSFID}/company/{companyID}/employee: get: summary: Get project company signatures for the employees description: Returns a list of employee project signature models when provided the project ID and company ID @@ -2145,7 +2424,7 @@ paths: - $ref: "#/parameters/x-username" - $ref: "#/parameters/x-email" - $ref: "#/parameters/path-projectSFID" - - $ref: "#/parameters/path-companySFID" + - $ref: "#/parameters/path-companyID" - $ref: '#/parameters/pageSize' - $ref: '#/parameters/nextKey' - $ref: '#/parameters/sortOrder' @@ -2169,7 +2448,7 @@ paths: tags: - signatures - /signatures/project/{projectSFID}/company/{companySFID}/clagroup/{claGroupID}/approval-list: + /signatures/project/{projectSFID}/company/{companyID}/clagroup/{claGroupID}/approval-list: put: summary: Updates the Project / Organization/Company Approval list description: API to update the project and organization/company approval list. @@ -2180,7 +2459,7 @@ paths: - $ref: "#/parameters/x-username" - $ref: "#/parameters/x-email" - $ref: "#/parameters/path-projectSFID" - - $ref: "#/parameters/path-companySFID" + - $ref: "#/parameters/path-companyID" - name: claGroupID in: path type: string @@ -2211,7 +2490,7 @@ paths: $ref: '#/responses/internal-server-error' tags: - signatures - + /company/{companySFID}/user/{userLFID}/claGroupID/{claGroupID}/is-cla-manager-designee: get: summary: Checks cla-manager-designee role @@ -2239,12 +2518,52 @@ paths: tags: - cla-manager - - /notify-cla-managers: - post: - summary: Send Notification to CLA Managaers - description: Send Notification to selected list of CLA Managers - security: [ ] + /signatures/company/{companyID}/clagroup/{claGroupID}/ecla-auto-create: + put: + summary: Updates CCLA signature record for the auto_create_ecla flag. + description: Updates CCLA signature record for the auto_create_ecla flag. + operationId: eclaAutoCreate + parameters: + - $ref: "#/parameters/x-request-id" + - $ref: "#/parameters/x-acl" + - $ref: "#/parameters/x-username" + - $ref: "#/parameters/x-email" + - $ref: "#/parameters/path-companyID" + - name: claGroupID + in: path + type: string + required: true + - name: body + in: body + schema: + $ref: '#/definitions/ecla-auto-create' + 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/signature' + '400': + $ref: '#/responses/invalid-request' + '401': + $ref: '#/responses/unauthorized' + '403': + $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' + '500': + $ref: '#/responses/internal-server-error' + tags: + - signatures + /notify-cla-managers: + post: + summary: Send Notification to CLA Managaers + description: Send Notification to selected list of CLA Managers + security: [ ] operationId: notifyCLAManagers parameters: - $ref: "#/parameters/x-request-id" @@ -2405,7 +2724,7 @@ paths: tags: - company - /company/{companySFID}/project/{projectSFID}/cla-manager/requests: + /company/{companyID}/project/{projectSFID}/cla-manager/requests: post: summary: Adds a CLA Manager Designee to the specified Company and Project description: User proposes a CLA Manager making the proposed user CLA Manager Designee @@ -2415,7 +2734,7 @@ paths: - $ref: "#/parameters/x-acl" - $ref: "#/parameters/x-username" - $ref: "#/parameters/x-email" - - $ref: "#/parameters/path-companySFID" + - $ref: "#/parameters/path-companyID" - $ref: "#/parameters/path-projectSFID" - name: body in: body @@ -2467,7 +2786,7 @@ paths: - cla-manager - /company/{companySFID}/project/{projectSFID}/cla-manager: + /company/{companyID}/project/{projectSFID}/cla-manager: post: summary: Adds a new CLA Manager to the specified Company and Project description: Allows an existing CLA Manager to add another CLA Manager to the specified Company and Project. @@ -2478,7 +2797,7 @@ paths: - $ref: "#/parameters/x-username" - $ref: "#/parameters/x-email" - $ref: "#/parameters/path-projectSFID" - - $ref: "#/parameters/path-companySFID" + - $ref: "#/parameters/path-companyID" - name: body in: body schema: @@ -2542,9 +2861,9 @@ paths: tags: - company - /company/{companySFID}/project/{projectSFID}/cla-manager/{userLFID}: + /company/{companyID}/project/{projectSFID}/cla-manager/{userLFID}: delete: - summary: Removes the CLA Manager from ACL for specified Company and Project + summary: Deletes the CLA Manager from CLA Manager list for specified Company and Project description: Allows an existing CLA Manager to remove another CLA Manager from the specified Company and Project. operationId: deleteCLAManager parameters: @@ -2553,7 +2872,7 @@ paths: - $ref: "#/parameters/x-username" - $ref: "#/parameters/x-email" - $ref: "#/parameters/path-projectSFID" - - $ref: "#/parameters/path-companySFID" + - $ref: "#/parameters/path-companyID" - $ref: "#/parameters/path-userLFID" responses: '204': @@ -2573,9 +2892,7 @@ paths: tags: - cla-manager - - - /company/{companySFID}/claGroup/{claGroupID}/cla-manager-designee: + /company/{companyID}/claGroup/{claGroupID}/cla-manager-designee: post: summary: Assigns CLA Manager designee description: Assigns CLA Manager designee to a given user @@ -2586,7 +2903,7 @@ paths: - $ref: "#/parameters/x-username" - $ref: "#/parameters/x-email" - $ref: "#/parameters/path-claGroupID" - - $ref: "#/parameters/path-companySFID" + - $ref: "#/parameters/path-companyID" - name: body in: body schema: @@ -2620,7 +2937,7 @@ paths: tags: - cla-manager - /company/{companySFID}/project/{projectSFID}/cla-manager-designee: + /company/{companyID}/project/{projectSFID}/cla-manager-designee: post: summary: Assigns CLA Manager designee description: Assigns CLA Manager designee to a given user @@ -2631,7 +2948,7 @@ paths: - $ref: "#/parameters/x-username" - $ref: "#/parameters/x-email" - $ref: "#/parameters/path-projectSFID" - - $ref: "#/parameters/path-companySFID" + - $ref: "#/parameters/path-companyID" - name: body in: body schema: @@ -2905,6 +3222,300 @@ paths: tags: - gerrits + # /cla-group/{claGroupID}/project/{projectSFID}/gerrits/icla/user: + # get: + # summary: Get Gerrit ICLA Users + # description: Gets the authorized individual CLA users from a gerrit instance for the CLA Group/Projecct + # operationId: getGerritICLAUser + # parameters: + # - $ref: "#/parameters/x-request-id" + # - $ref: "#/parameters/x-acl" + # - $ref: "#/parameters/x-username" + # - $ref: "#/parameters/x-email" + # - $ref: "#/parameters/path-claGroupID" + # - $ref: "#/parameters/path-projectSFID" + # 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/gerrit-group-response' + # '400': + # $ref: '#/responses/invalid-request' + # '403': + # $ref: '#/responses/forbidden' + # '409': + # $ref: '#/responses/conflict' + # '500': + # $ref: '#/responses/internal-server-error' + # tags: + # - gerrits + # put: + # summary: Add Gerrit ICLA Users + # description: Adds one or more individual CLA users to the gerrit CLA Group/project + # operationId: addGerritICLAUser + # parameters: + # - $ref: "#/parameters/x-request-id" + # - $ref: "#/parameters/x-acl" + # - $ref: "#/parameters/x-username" + # - $ref: "#/parameters/x-email" + # - $ref: "#/parameters/path-claGroupID" + # - $ref: "#/parameters/path-projectSFID" + # - in: body + # name: add-gerrit-user-input + # schema: + # $ref: '#/definitions/add-gerrit-user-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 + # '400': + # $ref: '#/responses/invalid-request' + # '403': + # $ref: '#/responses/forbidden' + # '409': + # $ref: '#/responses/conflict' + # '500': + # $ref: '#/responses/internal-server-error' + # tags: + # - gerrits + # delete: + # summary: Remove Gerrit ICLA Users + # description: Removes one or more individual CLA users from a gerrit instance for the CLA Group/Project + # operationId: removeGerritICLAUser + # parameters: + # - $ref: "#/parameters/x-request-id" + # - $ref: "#/parameters/x-acl" + # - $ref: "#/parameters/x-username" + # - $ref: "#/parameters/x-email" + # - $ref: "#/parameters/path-claGroupID" + # - $ref: "#/parameters/path-projectSFID" + # - in: body + # name: remove-gerrit-user-input + # schema: + # $ref: '#/definitions/remove-gerrit-user-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 + # '400': + # $ref: '#/responses/invalid-request' + # '403': + # $ref: '#/responses/forbidden' + # '409': + # $ref: '#/responses/conflict' + # '500': + # $ref: '#/responses/internal-server-error' + # tags: + # - gerrits + + # /cla-group/{claGroupID}/project/{projectSFID}/gerrits/ecla/user: + # get: + # summary: Get Gerrit ECLA Users + # description: Gets the authorized employee CLA users from a gerrit instance for the CLA Group/Projecct + # operationId: getGerritECLAUser + # parameters: + # - $ref: "#/parameters/x-request-id" + # - $ref: "#/parameters/x-acl" + # - $ref: "#/parameters/x-username" + # - $ref: "#/parameters/x-email" + # - $ref: "#/parameters/path-claGroupID" + # - $ref: "#/parameters/path-projectSFID" + # 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/gerrit-group-response' + # '400': + # $ref: '#/responses/invalid-request' + # '403': + # $ref: '#/responses/forbidden' + # '409': + # $ref: '#/responses/conflict' + # '500': + # $ref: '#/responses/internal-server-error' + # tags: + # - gerrits + # put: + # summary: Add Gerrit ECLA Users + # description: Adds one or more employee CLA users to a gerrit instance for the CLA Group/Project + # operationId: addGerritECLAUser + # parameters: + # - $ref: "#/parameters/x-request-id" + # - $ref: "#/parameters/x-acl" + # - $ref: "#/parameters/x-username" + # - $ref: "#/parameters/x-email" + # - $ref: "#/parameters/path-claGroupID" + # - $ref: "#/parameters/path-projectSFID" + # - in: body + # name: add-gerrit-user-input + # schema: + # $ref: '#/definitions/add-gerrit-user-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 + # '400': + # $ref: '#/responses/invalid-request' + # '403': + # $ref: '#/responses/forbidden' + # '409': + # $ref: '#/responses/conflict' + # '500': + # $ref: '#/responses/internal-server-error' + # tags: + # - gerrits + # delete: + # summary: Remove Gerrit ECLA Users + # description: Removes one or more employee CLA users from a gerrit instance for the project + # operationId: removeGerritECLAUser + # parameters: + # - $ref: "#/parameters/x-request-id" + # - $ref: "#/parameters/x-acl" + # - $ref: "#/parameters/x-username" + # - $ref: "#/parameters/x-email" + # - $ref: "#/parameters/path-claGroupID" + # - $ref: "#/parameters/path-projectSFID" + # - in: body + # name: remove-gerrit-user-input + # schema: + # $ref: '#/definitions/remove-gerrit-user-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 + # '400': + # $ref: '#/responses/invalid-request' + # '403': + # $ref: '#/responses/forbidden' + # '409': + # $ref: '#/responses/conflict' + # '500': + # $ref: '#/responses/internal-server-error' + # tags: + # - gerrits + /cla-group/{claGroupID}/user/{userID}/icla: + put: + summary: Invalidate ICLA record + description: Invalidates a given ICLA record for a user + operationId: invalidateICLA + parameters: + - $ref: "#/parameters/x-request-id" + - $ref: "#/parameters/x-acl" + - $ref: "#/parameters/x-email" + - $ref: "#/parameters/x-username" + - $ref: "#/parameters/path-claGroupID" + - $ref: "#/parameters/path-userID" + 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 + '400': + $ref: '#/responses/invalid-request' + '403': + $ref: '#/responses/forbidden' + '409': + $ref: '#/responses/conflict' + '500': + $ref: '#/responses/internal-server-error' + tags: + - signatures + + /company/{companyID}: + get: + summary: Get Company By Internal ID + description: Returns the company by internal ID + operationId: getCompanyByInternalID + parameters: + - $ref: "#/parameters/x-request-id" + - $ref: "#/parameters/x-acl" + - $ref: "#/parameters/x-username" + - $ref: "#/parameters/x-email" + - $ref: "#/parameters/path-companyID" + produces: + - application/json + 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/company' + '400': + $ref: '#/responses/invalid-request' + '401': + $ref: '#/responses/unauthorized' + '403': + $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' + tags: + - company + + /company/external/{companySFID}: + get: + summary: Get Company by External SFID + description: Returns the company by external ID + operationId: getCompanyByExternalID + parameters: + - $ref: "#/parameters/x-request-id" + - $ref: "#/parameters/x-acl" + - $ref: "#/parameters/x-username" + - $ref: "#/parameters/x-email" + - name: companySFID + in: path + type: string + required: true + produces: + - application/json + 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/company' + '400': + $ref: '#/responses/invalid-request' + '401': + $ref: '#/responses/unauthorized' + '403': + $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' + tags: + - company + /company/name/{companyName}: get: summary: Gets the company by name @@ -2940,6 +3551,41 @@ paths: tags: - company + /company/entityname/{signingEntityName}: + get: + summary: Gets the company by signing entity namename + description: Returns the matching company by signing entity name + operationId: getCompanyBySigningEntityName + parameters: + - $ref: "#/parameters/x-request-id" + - $ref: "#/parameters/x-acl" + - $ref: "#/parameters/x-username" + - $ref: "#/parameters/x-email" + - $ref: '#/parameters/path-signingEntityName' + produces: + - application/json + 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/company' + '400': + $ref: '#/responses/invalid-request' + '401': + $ref: '#/responses/unauthorized' + '403': + $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' + '500': + $ref: '#/responses/internal-server-error' + tags: + - company + /company/id/{companyID}: delete: summary: Deletes the company by ID @@ -2972,7 +3618,7 @@ paths: $ref: '#/responses/internal-server-error' tags: - company - + /company/lookup: get: summary: Search companies from organization service @@ -3047,7 +3693,7 @@ paths: tags: - company - /company/{companySFID}/project/{projectSFID}/cla-managers: + /company/{companyID}/project/{projectSFID}/cla-managers: get: summary: Get CLA manager of company for particular project/foundation description: Returns list CLA managers of the company for project/foundation @@ -3057,7 +3703,7 @@ paths: - $ref: "#/parameters/x-acl" - $ref: "#/parameters/x-username" - $ref: "#/parameters/x-email" - - $ref: "#/parameters/path-companySFID" + - $ref: "#/parameters/path-companyID" - $ref: "#/parameters/path-projectSFID" responses: '200': @@ -3109,7 +3755,7 @@ paths: tags: - company - /company/{companySFID}/project/{projectSFID}/active-cla-list: + /company/{companyID}/project/{projectSFID}/active-cla-list: get: summary: Get active CLA list of company for particular project/foundation description: Returns list active CLA of the company under particular project/foundation @@ -3119,7 +3765,7 @@ paths: - $ref: "#/parameters/x-acl" - $ref: "#/parameters/x-username" - $ref: "#/parameters/x-email" - - $ref: "#/parameters/path-companySFID" + - $ref: "#/parameters/path-companyID" - $ref: "#/parameters/path-projectSFID" responses: '200': @@ -3140,7 +3786,8 @@ paths: $ref: '#/responses/not-found' tags: - company - /company/{companySFID}/project/{projectSFID}/contributors: + + /company/{companyID}/project/{projectSFID}/contributors: get: summary: Get corporate contributors for project description: Returns list of corporate contributors for project @@ -3150,9 +3797,11 @@ paths: - $ref: "#/parameters/x-acl" - $ref: "#/parameters/x-username" - $ref: "#/parameters/x-email" - - $ref: "#/parameters/path-companySFID" + - $ref: "#/parameters/path-companyID" - $ref: "#/parameters/path-projectSFID" - $ref: "#/parameters/searchTerm" + - $ref: '#/parameters/nextKey' + - $ref: '#/parameters/pageSize' responses: '200': description: 'Success' @@ -3206,6 +3855,7 @@ paths: - $ref: "#/parameters/x-email" - $ref: "#/parameters/path-companySFID" - $ref: "#/parameters/path-projectSFID" + - $ref: "#/parameters/companyID" responses: '200': description: 'Success' @@ -3327,6 +3977,377 @@ paths: tags: - github-activity + /gitlab/oauth/callback: + get: + summary: The endpoint is called after user authorizes EasyCLA bot + description: The endpoint is responsible for storing the access token for the user and registering the webhooks is autoenable is on + security: [ ] + operationId: gitlabOauthCallback + parameters: + - name: code + description: oauth code used to fetch the access token + in: query + type: string + - name: state + description: state is used to find the gitlab organization + in: query + type: string + 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/success-response' + '400': + $ref: '#/responses/invalid-request' + '403': + $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' + '500': + $ref: '#/responses/internal-server-error' + tags: + - gitlab-activity + + /gitlab/user/oauth/callback: + get: + summary: The endpoint is called after user is authorized for the sign flow + description: The endpoint handles storing the OAuth2 session information for this user and initiate the signing workflow + security: [ ] + operationId: gitlabUserOauthCallback + parameters: + - $ref: "#/parameters/x-request-id" + - name: code + description: oauth code used to fetch the access token + in: query + type: string + required: true + - name: state + description: state is used to find the gitlab user + in: query + type: string + 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/success-response' + '400': + $ref: '#/responses/invalid-request' + '403': + $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' + '500': + $ref: '#/responses/internal-server-error' + tags: + - gitlab-activity + + /gitlab/activity: + post: + summary: Gitlab Activity Callback Handler + description: Gitlab Activity Callback Handler reacts to Gitlab events emmited. + security: [ ] + operationId: gitlabActivity + parameters: + - $ref: "#/parameters/x-request-id" + - $ref: "#/parameters/x-gitlab-token" + - name: gitlabActivityInput + in: body + schema: + $ref: '#/definitions/gitlab-activity-input' + responses: + '200': + description: 'Success' + '400': + $ref: '#/responses/invalid-request' + '401': + $ref: '#/responses/unauthorized' + '403': + $ref: '#/responses/forbidden' + '500': + $ref: '#/responses/internal-server-error' + tags: + - gitlab-activity + + /gitlab/trigger: + post: + summary: Gitlab Activity MR Trigger + description: Endpoint is used to trigger specific MR for gitlab + security: [ ] + operationId: gitlabTrigger + parameters: + - $ref: "#/parameters/x-request-id" + - name: gitlabTriggerInput + in: body + schema: + $ref: '#/definitions/gitlab-trigger-input' + responses: + '200': + description: 'Success' + '400': + $ref: '#/responses/invalid-request' + '401': + $ref: '#/responses/unauthorized' + '403': + $ref: '#/responses/forbidden' + '500': + $ref: '#/responses/internal-server-error' + tags: + - gitlab-activity + + + /repository-provider/gitlab/sign/{organizationID}/{gitlabRepositoryID}/{mergeRequestID}: + get: + summary: Gitlab sign request handler + description: Endpoint that will initiate a CLA Signature for the User + security: [ ] + operationId: signRequest + parameters: + - $ref: "#/parameters/x-request-id" + - $ref: "#/parameters/path-gitlabOrganizationID" + - $ref: "#/parameters/path-gitlabRepositoryID" + - $ref: "#/parameters/path-mergeRequestID" + responses: + '200': + description: 'Success' + '400': + $ref: '#/responses/invalid-request' + '500': + $ref: '#/responses/internal-server-error' + 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/individual-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 + + /signed/individual/{installation_id}/{github_repository_id}/{change_request_id}: + post: + summary: Endpoint to receive DocuSign callback for signed documents. + description: Receives XML data when an individual signs a document in DocuSign. + security: [ ] + operationId: iclaCallbackGithub + consumes: + - text/xml + parameters: + - $ref: "#/parameters/x-request-id" + - in: header + name: accept-encoding + type: string + required: false + default: gzip + - in: header + name: connection + type: string + required: false + default: Keep-Alive + - in: header + name: content-type + type: string + required: true + default: 'text/xml; charset=utf-8' + - in: header + name: user-agent + type: string + required: false + default: docusign + - name: installation_id + in: path + required: true + type: string + - name: github_repository_id + in: path + required: true + type: string + - name: change_request_id + in: path + required: true + type: string + - name: envelopeInformation + in: body + required: true + description: XML payload with DocuSign envelope information + schema: + $ref: '#/definitions/DocuSignEnvelopeInformation' + responses: + '200': + description: Successfully received and processed the callback data. + '400': + description: Invalid request. + '415': + description: Invalid format. + tags: + - sign + + /signed/gerrit/individual/{user_id}: + post: + summary: Endpoint to receive DocuSign callback for signed documents from Gerrit. + description: Receives XML data when an individual signs a document in DocuSign linked to Gerrit. + operationId: iclaCallbackGerrit + security: [ ] + consumes: + - text/xml + parameters: + - $ref: "#/parameters/x-request-id" + - name: user_id + in: path + required: true + type: string + - name: body + in: body + required: true + schema: + type: object + additionalProperties: true + responses: + '200': + description: Successfully received and processed the Gerrit callback data. + '400': + description: Invalid request. + tags: + - sign + + /signed/corporate/{project_id}/{company_id}: + post: + summary: Endpoint to receive DocuSign callback for signed corporate documents. + description: Receives XML data when a corporate entity signs a document in DocuSign associated with a specific project. + operationId: cclaCallback + security: [ ] + consumes: + - text/xml + parameters: + - $ref: "#/parameters/x-request-id" + - name: project_id + in: path + required: true + type: string + - name: company_id + in: path + required: true + type: string + - name: body + in: body + required: true + schema: + type: object + additionalProperties: true + responses: + '200': + description: Successfully received and processed the callback data for corporate signatures. + '400': + description: Invalid request. + tags: + - sign + + /signed/gitlab/individual/{user_id}/{organization_id}/{gitlab_repository_id}/{merge_request_id}: + post: + summary: Endpoint for DocuSign callback for GitLab individual signatures. + description: Receives XML data when an individual signs a document in DocuSign linked to GitLab. + operationId: iclaCallbackGitlab + security: [ ] + consumes: + - text/xml + parameters: + - $ref: "#/parameters/x-request-id" + - name: user_id + in: path + required: true + type: string + - name: organization_id + in: path + required: true + type: string + - name: gitlab_repository_id + in: path + required: true + type: string + - name: merge_request_id + in: path + required: true + type: string + - name: envelopeInformation + in: body + required: true + description: XML payload with DocuSign envelope information + schema: + $ref: '#/definitions/DocuSignEnvelopeInformation' + responses: + '200': + description: Callback data for GitLab successfully received and processed. + '400': + description: Invalid request. + tags: + - sign + /cla/authorization: + get: + summary: check if LFID is authorized for a CLA Group ID + description: This endpoint checks if a given LFID is authorized for a specific CLA Group + operationId: isAuthorized + security: [ ] + parameters: + - $ref: "#/parameters/x-request-id" + - name: lfid + in: query + required: true + type: string + description: The Linux Foundation ID user + - name: claGroupId + in: query + required: true + type: string + description: The CLA Group ID + responses: + '200': + description: Authorization status of the LFID for the specified CLA Group + 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/lfid-authorized-response' + + '400': + $ref: '#/responses/invalid-request' + + tags: + - signatures + + + responses: unauthorized: description: Unauthorized @@ -3368,6 +4389,14 @@ responses: description: The unique request ID value - assigned/set by the API Gateway based on the session schema: $ref: '#/definitions/error-response' + service-unavailable: + description: Service unavailable + 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/error-response' conflict: description: Duplicate Resource headers: @@ -3436,6 +4465,18 @@ parameters: required: false # UUID v4 regex # pattern: '[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}' + approved: + name: approved + description: The signature approved query parameter. If set with a value of true, the query would return approved signatures. If set with a value of false, the query would return invalidated/disabled signatures. + in: query + type: boolean + required: false + signed: + name: signed + description: The signature signed query parameter. If set with a value of true, the query would return signed signatures. If set with a value of false, the query would return incomplete/unsigned signatures. + in: query + type: boolean + required: false sortOrder: name: sortOrder description: The sort order - either asc or desc @@ -3449,8 +4490,7 @@ parameters: in: query type: string required: false - pattern: '^([\w\d\s\-\,\./]+){2,255}$' - #$ref: './common/properties/company-name.yaml' + pattern: '[^<>]*' # allow everything except greater than and less than symbols userName: name: userName description: The optional user name filter @@ -3460,10 +4500,15 @@ parameters: pattern: '^\w+$' signatureType: name: signatureType + description: > + CLA Type query parameter - allows the caller to specify either individual, employee or corporate signature, valid options: + * `icla` - for individual contributor signature records (individuals not associated with a corporation) + * `ecla` - for employee contributor signature records (acknowledgements from corporate contributors) + * `ccla` - for corporate contributor signature records (created by CLA Signatories and managed by CLA Managers) in: query type: string required: false - enum: [ ccla,cla ] + enum: [ ccla,ecla,cla ] claType: name: claType description: > @@ -3474,7 +4519,7 @@ parameters: in: query type: string required: false - enum: [ icla,ecla,ccla ] + enum: [ ccla,ecla,icla ] templateCLAType: name: claType in: query @@ -3486,14 +4531,19 @@ parameters: description: the Salesforce ID of the Foundation in: query type: string + pattern: '^[a-zA-Z0-9]{18}|[a-zA-Z0-9]{15}$' # see: https://stackoverflow.com/questions/9742913/validating-a-salesforce-id + companyID: + name: companyID + description: The internal company ID representing signing entity name instance (EasyCLA) + in: query + type: string path-claGroupID: name: claGroupID description: ID of the CLA Group in: path type: string required: true - # \w - Any word character (alphanumeric & underscore), dashes, periods - pattern: '^(\w)([\w\-.])+$' + pattern: '^[a-fA-F0-9]{8}-?[a-fA-F0-9]{4}-?4[a-fA-F0-9]{3}-?[89ab][a-fA-F0-9]{3}-?[a-fA-F0-9]{12}$' # uuidv4 minLength: 5 maxLength: 255 path-foundationSFID: @@ -3502,8 +4552,7 @@ parameters: in: path type: string required: true - # \w - Any word character (alphanumeric & underscore), dashes, periods - pattern: '^(\w)([\w\-.])+$' + pattern: '^[a-zA-Z0-9]{18}|[a-zA-Z0-9]{15}$' # see: https://stackoverflow.com/questions/9742913/validating-a-salesforce-id minLength: 5 maxLength: 255 path-projectSFID: @@ -3512,8 +4561,7 @@ parameters: in: path type: string required: true - # \w - Any word character (alphanumeric & underscore), dashes, periods - pattern: '^(\w)([\w\-.])+$' + pattern: '^[a-zA-Z0-9]{18}|[a-zA-Z0-9]{15}$' # see: https://stackoverflow.com/questions/9742913/validating-a-salesforce-id minLength: 5 maxLength: 255 path-userID: @@ -3538,42 +4586,60 @@ parameters: in: path type: string required: true - # \w - Any word character (alphanumeric & underscore), dashes, periods - pattern: '^(\w)([\w\-.])+$' - minLength: 5 - maxLength: 255 + pattern: '^[a-fA-F0-9]{8}-?[a-fA-F0-9]{4}-?4[a-fA-F0-9]{3}-?[89ab][a-fA-F0-9]{3}-?[a-fA-F0-9]{12}$' # uuidv4 path-companySFID: name: companySFID description: salesforce id of the company in: path type: string required: true - # \w - Any word character (alphanumeric & underscore), dashes, periods - pattern: '^(\w)([\w\-.])+$' - minLength: 5 - maxLength: 255 + pattern: '^[a-zA-Z0-9]{18}|[a-zA-Z0-9]{15}$' # see: https://stackoverflow.com/questions/9742913/validating-a-salesforce-id path-companyName: name: companyName description: the company name in: path type: string required: true - pattern: '^([\w\d\s\-\,\./]+){2,255}$' + pattern: '[^<>]*' # allow everything except greater than and less than symbols + minLength: 2 + maxLength: 100 + path-signingEntityName: + name: signingEntityName + type: string + description: Signing Entity Name of the Company + # Pattern aligns with UI and other platform services including Org Service + pattern: '[^<>]*' # allow everything except greater than and less than symbols + minLength: 2 + maxLength: 100 + in: path + required: true path-signatureID: name: signatureID description: id of the CLA signature in: path type: string required: true - # \w - Any word character (alphanumeric & underscore), dashes, periods - pattern: '^(\w)([\w\-.])+$' + pattern: '^[a-fA-F0-9]{8}-?[a-fA-F0-9]{4}-?4[a-fA-F0-9]{3}-?[89ab][a-fA-F0-9]{3}-?[a-fA-F0-9]{12}$' # uuidv4 minLength: 5 maxLength: 255 - companySFID: - name: companySFID - description: salesforce id of the company - in: query + path-gitlabOrganizationID: + name: organizationID + description: GitLab organization ID + type: string + in: path + required: true + path-mergeRequestID: + name: mergeRequestID + description: GitLab Merge Request identifier type: string + in: path + required: true + path-gitlabRepositoryID: + name: gitlabRepositoryID + type: string + description: GitLab Repository/Project identifier + in: path + required: true gerritHost: name: gerritHost description: host of the gerrit server @@ -3623,6 +4689,12 @@ parameters: description: Github event signature which is used for validation of the request body in: header type: string + x-gitlab-token: + name: X-Gitlab-Token + description: Gitlab webhook secret token sent for futher verification + in: header + type: string + required: true definitions: # Common definitions @@ -3642,6 +4714,65 @@ definitions: event: $ref: './common/event.yaml' + #-------------------------------------- + # Docusign Webhook Payload + #____________________________________________ + DocuSignEnvelopeInformation: + type: object + properties: + EnvelopeStatus: + type: object + properties: + EnvelopeID: + type: string + Status: + type: string + RecipientStatuses: + type: array + items: + $ref: '#/definitions/RecipientStatus' + FormData: + type: object + properties: + xfdf: + type: object + properties: + fields: + type: array + items: + $ref: '#/definitions/Field' + xml: + name: DocuSignEnvelopeInformation + + RecipientStatus: + type: object + properties: + Type: + type: string + Email: + type: string + UserName: + type: string + Status: + type: string + ClientUserId: + type: string + xml: + name: RecipientStatus + + Field: + type: object + properties: + name: + type: string + value: + type: string + xml: + name: field + + # --------------------------------------------------------------------------- + # GitHub Definitions + # --------------------------------------------------------------------------- github-activity-input: type: object required: @@ -3651,19 +4782,53 @@ definitions: type: string additionalProperties: true + gitlab-activity-input: + type: object + properties: + object_kind: + type: string + additionalProperties: true + + gitlab-trigger-input: + type: object + required: + - gitlab_organization_id + - gitlab_external_repository_id + - gitlab_mr_id + properties: + gitlab_organization_id: + type: string + description: the gitlab organization id to which mr belongs to + gitlab_external_repository_id: + type: integer + description: gitlab project identifier associated with mr + gitlab_mr_id: + type: integer + description: gitlab mr id + github-repository-input: type: object required: - - repository_github_id - github_organization_name - cla_group_id properties: + repository_github_ids: + type: array + items: + description: the repository external identifier, such as the GitHub ID of the repo + type: string + example: '337730995' repository_github_id: type: string + description: the repository external identifier, such as the GitHub ID of the repo + example: '337730995' github_organization_name: type: string + description: the repository organization + example: 'cncf' cla_group_id: - type: string + description: CLA Group ID + $ref: './common/properties/internal-id.yaml' github-repository-branch-protection-status-checks: type: object @@ -3701,6 +4866,8 @@ definitions: github-repository-branch-protection-input: type: object properties: + branch_name: + type: string enforce_admin: type: boolean default: false @@ -3709,12 +4876,51 @@ definitions: items: $ref: '#/definitions/github-repository-branch-protection-status-checks' + github-organization: + $ref: './common/github-organization.yaml' + github-repository: $ref: './common/github-repository.yaml' - list-github-repositories: - $ref: './common/list-github-repositories.yaml' + github-create-organization: + $ref: './common/github-organization-create.yaml' + + github-update-organization: + $ref: './common/github-organization-update.yaml' + + github-list-repositories: + $ref: './common/github-repositories-list.yaml' + + # --------------------------------------------------------------------------- + # GitLab Definitions + # --------------------------------------------------------------------------- + gitlab-organization: + $ref: './common/gitlab-organization.yaml' + + gitlab-create-organization: + $ref: './common/gitlab-organization-create.yaml' + + gitlab-organization-update: + $ref: './common/gitlab-organization-update.yaml' + + gitlab-repository: + $ref: './common/gitlab-repository.yaml' + + gitlab-repositories-list: + $ref: './common/gitlab-repositories-list.yaml' + + gitlab-repositories-enroll: + $ref: './common/gitlab-repositories-enroll.yaml' + + gitlab-group-member: + $ref: './common/gitlab-group-member.yaml' + + gitlab-group-members-list: + $ref: './common/gitlab-group-members-list.yaml' + # --------------------------------------------------------------------------- + # CLA Group Definitions + # --------------------------------------------------------------------------- cla-groups: $ref: './common/cla-groups.yaml' @@ -3724,6 +4930,9 @@ definitions: sf-project-summary: $ref: './common/sf-project-summary.yaml' + # --------------------------------------------------------------------------- + # CLA Template Definitions + # --------------------------------------------------------------------------- template: $ref: './common/template.yaml' @@ -3733,18 +4942,6 @@ definitions: template-pdfs: $ref: './common/template-pdfs.yaml' - github-organizations: - $ref: './common/github-organizations.yaml' - - github-organization: - $ref: './common/github-organization.yaml' - - create-github-organization: - $ref: './common/create-github-organization.yaml' - - update-github-organization: - $ref: './common/update-github-organization.yaml' - user: $ref: './common/user.yaml' @@ -3753,6 +4950,15 @@ definitions: signature: $ref: './common/signature.yaml' + + corporate-signatures: + $ref: './common/corporate-signatures.yaml' + + corporate-signature: + $ref: './common/corporate-signature.yaml' + + approval-item: + $ref: './common/approval-item.yaml' icla-signatures: $ref: './common/icla-signatures.yaml' @@ -3772,6 +4978,15 @@ definitions: add-gerrit-input: $ref: './common/add-gerrit-input.yaml' + gerrit-group-response: + $ref: './common/gerrit-group-response.yaml' + + add-gerrit-user-input: + $ref: './common/gerrit-user-list.yaml' + + remove-gerrit-user-input: + $ref: './common/gerrit-user-list.yaml' + gerrit-repo: $ref: './common/gerrit-repo.yaml' @@ -3799,8 +5014,8 @@ definitions: github-repository-info: $ref: './common/github-repository-info.yaml' - #company: - # $ref: './common/company.yaml' + gitlab-repository-info: + $ref: './common/gitlab-repository-info.yaml' total-count-metrics: type: object @@ -3918,7 +5133,6 @@ definitions: - userEmail - companyName - companyWebsite - - signingEntityName properties: companyName: $ref: './common/properties/company-name.yaml' @@ -3929,6 +5143,10 @@ definitions: description: the company website userEmail: $ref: './common/properties/email.yaml' + note: + description: 'Optional note associated with the new company request. This information will be attached to the company record.' + type: string + maxLength: 256 company-output: type: object @@ -3967,24 +5185,24 @@ definitions: Source: type: string description: >- - The company account source, such as "Google Natural Search", "Event-Promo", "Direct Mail", or "Tradeshow". - If the information was found in clearbit, this value will be "clearbit". + The company account source, such as "Google Natural Search", "Event-Promo", "Direct Mail", or "Tradeshow". + If the information was found in clearbit, this value will be "clearbit". example: "clearbit" Industry: type: string description: >- - The company industry, such as "Banking" or "Communications" + The company industry, such as "Banking" or "Communications" example: "Education" pattern: '^([\w\d\s\-\,\.]+){2,40}$' Sector: type: string description: >- - The company industry sector, such as "Information Technology" + The company industry sector, such as "Information Technology" example: "Information Technology" Employees: type: string description: The number of employees of the company - example: "500 - 4999, 10000+" + example: "500 - 4999" pattern: '^([\w\d\s\-\,\.\/]+){2,}$' signingEntityNames: type: array @@ -4121,8 +5339,8 @@ definitions: items: $ref: '#/definitions/cla-manager-designee' - - + + user-role-status: type: object title: User Role status @@ -4136,17 +5354,6 @@ definitions: companySFID: type: string - contributors: - type: object - title: contributors - description: List of contributor roles for a user - properties: - list: - type: array - items: - $ref: - '#/definitions/contributor' - contributor: type: object title: Contributor @@ -4178,42 +5385,6 @@ definitions: example: 'contributor' x-omitempty: false - company-owner: - type: object - title: Company Owner - description: Company Owner - properties: - lf_username: - type: string - description: 'the LF username' - x-omitempty: false - example: 'username' - name: - type: string - description: 'name of the user' - example: 'john' - x-omitempty: false - user_sfid: - type: string - description: 'the user SalesForce ID' - x-omitempty: false - email: - $ref: './common/properties/email.yaml' - x-omitempty: false - type: - type: string - x-omitempty: false - example: 'contact' - assigned_on: - type: string - x-omitempty: false - company_sfid: - type: string - description: 'the Organization SalesForce ID' - x-omitempty: false - example: 'abc134234adsdf43' - - cla-manager-designee: type: object title: Company CLA Designee @@ -4243,20 +5414,20 @@ definitions: assigned_on: type: string x-omitempty: false + company_id: + $ref: './common/properties/internal-id.yaml' + description: 'the Company/Organization internal ID' + x-omitempty: false company_sfid: - type: string - description: 'the Organization SalesForce ID' + $ref: './common/properties/external-id.yaml' + description: 'the Company/Organization SalesForce ID' x-omitempty: false - example: 'abc134234adsdf43' project_sfid: - type: string + $ref: './common/properties/external-id.yaml' description: 'the project SalesForce ID' x-omitempty: false - example: 'a2g17000000hyxNAAA' project_name: - type: string - description: 'name of the salesforce project' - example: 'Appium' + $ref: './common/properties/project-name.yaml' x-omitempty: false company-cla-manager: @@ -4288,31 +5459,32 @@ definitions: type: string x-omitempty: false project_id: - type: string + $ref: './common/properties/internal-id.yaml' description: "The Project ID" x-omitempty: false - example: "e1e30240-a722-4c82-a648-121681d959c7" project_sfid: - type: string + $ref: './common/properties/external-id.yaml' description: "The Project SalesForce ID" x-omitempty: false - example: "a2g17000000hyxNAAA" project_name: - type: string - description: "The name of the SalesForce project" - example: "Appium" + $ref: './common/properties/cla-group-name.yaml' x-omitempty: false cla_group_name: $ref: './common/properties/cla-group-name.yaml' organization_name: - type: string - description: "The name of Salesforce organization" + $ref: './common/properties/company-name.yaml' + x-omitempty: false + signing_entity_name: + $ref: './common/properties/company-signing-entity-name.yaml' + x-omitempty: false + organization_id: + $ref: './common/properties/internal-id.yaml' + description: "The internal organization ID" x-omitempty: false - example: "Intel Corporation" organization_sfid: - type: string + $ref: './common/properties/external-id.yaml' + description: "The Salesforce organization ID" x-omitempty: false - example: "00117000015vpjXAAQ" cla-manager-user: type: object @@ -4406,7 +5578,7 @@ definitions: notify-cla-manager-list: type: object - title: Cla Manager list and contributor userID for given company and Project + title: CLA Manager list and contributor userID for given company and Project description: list of CLA Manager emails and contributor userID properties: list: @@ -4417,8 +5589,13 @@ definitions: type: string companyName: $ref: './common/properties/company-name.yaml' - claGroupName: - $ref: './common/properties/cla-group-name.yaml' + signingEntityName: + $ref: './common/properties/company-signing-entity-name.yaml' + claGroupID: + title: CLA Group ID + description: The CLA Group ID + $ref: './common/properties/internal-id.yaml' + x-omitempty: false notify-cla-manager: type: object @@ -4430,6 +5607,14 @@ definitions: type: string company-project-cla-list: + type: object + properties: + list: + type: array + items: + $ref: '#/definitions/company-project-cla' + + company-project-cla: type: object properties: signed_cla_list: @@ -4470,6 +5655,18 @@ definitions: title: unsigned project description: details of unsigned project properties: + company_name: + type: string + description: The company name + x-omitempty: false + example: "The Linux Foundation" + signing_entity_name: + type: string + description: The company signing entity name + x-omitempty: false + example: "The Linux Foundation Subsidiary 1" + signing_entity_id: + $ref: './common/properties/internal-id.yaml' cla_group_id: type: string x-omitempty: false @@ -4505,20 +5702,46 @@ definitions: title: Active CLA of the company description: Details of the active CLA Group properties: + company_name: + type: string + description: The company name + x-omitempty: false + example: "The Linux Foundation" + company_id: + $ref: './common/properties/internal-id.yaml' + description: 'the Company ID' + x-omitempty: false + company_sfid: + $ref: './common/properties/external-id.yaml' + description: 'the Company/Organization SalesForce ID' + x-omitempty: false + signing_entity_name: + type: string + description: The company signing entity name + x-omitempty: false + example: "The Linux Foundation Subsidiary 1" + signing_entity_id: + $ref: './common/properties/internal-id.yaml' + signature_acl: + $ref: './common/signature-acl.yaml' signed_on: type: string x-omitempty: false example: "2019-07-18T11:38:13.144674+0000" project_id: - type: string - description: The CLA Group/Project ID + title: CLA Group ID + description: The CLA Group/Project ID - here for backwards compatiablity + $ref: './common/properties/internal-id.yaml' + x-omitempty: false + cla_group_id: + title: CLA Group ID + description: The CLA Group ID + $ref: './common/properties/internal-id.yaml' x-omitempty: false - example: "e1e30240-a722-4c82-a648-121681d959c7" project_sfid: - type: string + $ref: './common/properties/external-id.yaml' description: The project SalesForce ID x-omitempty: false - example: "a2g17000000hyxNAAA" project_name: type: string description: The project name @@ -4539,11 +5762,14 @@ definitions: example: "John Doe" x-omitempty: false signature_id: - type: string - example: "55ec4162-9e41-47da-a643-f81666953a51" + $ref: './common/properties/external-id.yaml' + title: Signature ID + description: The internal signature ID x-omitempty: false project_logo: type: string + title: Project Logo + description: the project logo example: "http://wwwlinuxfoundation.org/logo.gif" x-omitempty: false cla_group_name: @@ -4559,6 +5785,28 @@ definitions: corporate-contributor: $ref: './common/corporate-contributor.yaml' + individual-signature-input: + type: object + required: + - project_id + - user_id + properties: + project_id: + type: string + 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: type: object required: @@ -4583,12 +5831,13 @@ definitions: description: send signing request as email. This should be set to true when requestor is not signatory. authority_name: type: string - example: 'John Doe' - description: name of the cla signatory - pattern: "^[a-zA-Z0-9]+(([',. -][a-zA-Z0-9 ])?[a-zA-Z0-9]*)*$" + example: "Derk Miyamoto" + description: the name of the CLA signatory + minLength: 2 + maxLength: 255 authority_email: $ref: './common/properties/email.yaml' - description: Email of the CLA Signatory + description: the email of the CLA Signatory return_url: type: string example: 'https://corporate.dev.lfcla.com/#/company/eb4d7d71-693f-4047-bf8d-10d0e7764969' @@ -4605,6 +5854,22 @@ 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 + user_id: + type: string + description: easyCLA user identification + project_id: + type: string + description: clagroup ID + signed_document: type: object properties: @@ -4672,83 +5937,10 @@ definitions: $ref: '#/definitions/cla-group-summary' cla-group-summary: - type: object - properties: - foundationLevelCLA: - description: Flag indicating whether CLA is signed at Foundation level (true) or Project level (false) - type: boolean - x-omitempty: false - cla_group_id: - type: string - example: 'b1e86e26-d8c8-4fd8-9f8d-5c723d5dac9f' - description: id of the CLA group - x-omitempty: false - cla_group_name: - $ref: './common/properties/cla-group-name.yaml' - x-omitempty: false - cla_group_description: - $ref: './common/properties/cla-group-description.yaml' - x-omitempty: false - ccla_enabled: - type: boolean - example: true - description: flag to indicate if CCLA is enabled - x-omitempty: false - ccla_requires_icla: - type: boolean - example: true - description: flag to indicate if corporate contributors requires to sign ICLA - x-omitempty: false - icla_enabled: - type: boolean - example: true - description: flag to indicate if ICLA is enabled - x-omitempty: false - foundation_sfid: - type: string - example: 'a09410000182dD2AAI' - description: foundation sfid under which this CLA group is created - x-omitempty: false - root_project_repositories_count: - type: integer - description: number of repositories added to this CLA Group from root project - x-omitempty: false - foundation_name: - type: string - example: 'Academy Software Foundation' - description: foundation name under which this CLA group is created - x-omitempty: false - repositories_count: - type: integer - description: total repositories under this cla-group - x-omitempty: false - total_signatures: - type: integer - description: aggregate count of ICLA and CCLA contributors within this CLA Group - x-omitempty: false - project_list: - x-omitempty: false - description: list of projects under foundation for which this CLA group is created - type: array - items: - $ref: '#/definitions/cla-group-project' - icla_pdf_url: - description: template URL for ICLA document - type: string - example: 'https://cla-signature-files-dev.s3.amazonaws.com/contract-group/b1e86e26-d8c8-4fd8-9f8d-5c723d5dac9f/template/icla.pdf' - x-omitempty: false - ccla_pdf_url: - description: template URL for CCLA document - type: string - example: 'https://cla-signature-files-dev.s3.amazonaws.com/contract-group/b1e86e26-d8c8-4fd8-9f8d-5c723d5dac9f/template/ccla.pdf' - x-omitempty: false - setup_completion_pct: - description: the CLA Group setup complete percentage, values range from 0-100 inclusive - type: integer - minimum: 0 - maximum: 100 - example: 100 - x-omitempty: false + $ref: './common/cla-group-summary.yaml' + + document-tab: + $ref: './common/document-tab.yaml' cla-group-project: type: object @@ -4823,6 +6015,17 @@ definitions: items: type: string + gitlab-organizations: + $ref: './common/gitlab-organizations.yaml' + + ecla-auto-create: + type: object + properties: + auto_create_ecla: + type: boolean + description: flag to indicate if the product should automatically create an employee acknowledgement for a given user when the CLA manager adds the user to the email, GitLab username, or GitLab username approval list + example: true + project-github-organizations: type: object properties: @@ -4848,6 +6051,11 @@ definitions: type: boolean description: Flag to indicate if this GitHub Organization is configured to automatically setup branch protection on CLA enabled repositories. x-omitempty: false + installationURL: + type: string + x-nullable: true + example: "https://github.com/organizations/deal-test-org-2/settings/installations/1235464" + format: uri github_organization_name: type: string description: The GitHub Organization name @@ -4898,6 +6106,15 @@ definitions: - connected - connection_failure + gitlab-project-organizations: + $ref: './common/gitlab-project-organizations.yaml' + + gitlab-project-organization: + $ref: './common/gitlab-project-organization.yaml' + + gitlab-project-repository: + $ref: './common/gitlab-project-repository.yaml' + url-object: type: object properties: @@ -4942,3 +6159,39 @@ definitions: description: The unique request ID value - assigned/set by the API Gateway or the API based on the login session example: 'b1e86e26-d8c8-4fd8-9f8d-5c723d5dac9f' type: string + + lfid-authorized-response: + type: object + properties: + authorized: + type: boolean + description: Indicates if the LFID is authorized for the CLA Group. + x-omitempty: false + lfid: + type: string + description: The LFID that was checked + claGroupId: + type: string + description: The ID of the Group + companyID: + type: string + description: The ID of the company associated with the User (optional) + CCLARequiresICLA: + type: boolean + description: Flag ensuring user signs ICLA and is acknowledged the company + x-omitempty: false + companyAffiliation: + type: boolean + description: User is affiliated with a company (use case when user has no companyID attribute) + x-omitempty: false + ICLA: + type: boolean + description: User has an ICLA signature + x-omitempty: false + CCLA: + type: boolean + description: Flag to indicate if user has been company acknowledged and approved + x-omitempty: false + + + diff --git a/cla-backend-go/swagger/common/add-gerrit-input.yaml b/cla-backend-go/swagger/common/add-gerrit-input.yaml index eb38ce96d..858bafa89 100644 --- a/cla-backend-go/swagger/common/add-gerrit-input.yaml +++ b/cla-backend-go/swagger/common/add-gerrit-input.yaml @@ -15,7 +15,7 @@ properties: pattern: '^[\w\p{L}][\w\s\p{L}\[\]\+\-\{\}\(\)\.\,\+\-]*$' gerritUrl: description: | - the gerrit url - must be one of the currently supported LF managed Gerrit instances: + the gerrit url - should be one of the supported LF managed Gerrit instances, examples are: https://gerrit.linuxfoundation.org https://gerrit.onap.org https://gerrit.o-ran-sc.org @@ -23,24 +23,9 @@ properties: https://gerrit.opnfv.org example: 'https://gerrit.onap.org' type: string - enum: - - https://gerrit.linuxfoundation.org - - https://gerrit.onap.org - - https://gerrit.o-ran-sc.org - - https://gerrit.tungsten.io - - https://gerrit.opnfv.org - groupIdCcla: - type: string - description: the LDAP group ID for CCLA - example: '1902' - minLength: 3 - maxLength: 12 - groupIdIcla: - type: string - description: the LDAP group ID for ICLA - example: '1903' - minLength: 3 - maxLength: 12 + minLength: 10 + maxLength: 255 + pattern: ^(?:http(s)?:\/\/).+$ version: type: string description: the version associated with the gerrit record diff --git a/cla-backend-go/swagger/common/approval-item.yaml b/cla-backend-go/swagger/common/approval-item.yaml new file mode 100644 index 000000000..70d164255 --- /dev/null +++ b/cla-backend-go/swagger/common/approval-item.yaml @@ -0,0 +1,10 @@ + +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +properties: + approval_item: + type: string + date_added: + type: string \ No newline at end of file diff --git a/cla-backend-go/swagger/common/cla-group-document.yaml b/cla-backend-go/swagger/common/cla-group-document.yaml index 8f6d9e8bb..70378861c 100644 --- a/cla-backend-go/swagger/common/cla-group-document.yaml +++ b/cla-backend-go/swagger/common/cla-group-document.yaml @@ -30,6 +30,9 @@ properties: description: the document content type example: 'storage+pdf' type: string + documentContent: + description: the document content + type: string documentS3URL: description: the document S3 URL example: "https://cla-signature-files-dev.s3.amazonaws.com/contract-group/f7222222-7777-4444-aaaa-1c1c1c1c1c1c/template/ccla.pdf" @@ -46,3 +49,7 @@ properties: description: the document creation date example: '2019-08-01T06:55:09Z' type: string + documentTabs: + type: array + items: + $ref: '#/definitions/document-tab' diff --git a/cla-backend-go/swagger/common/cla-group-summary.yaml b/cla-backend-go/swagger/common/cla-group-summary.yaml new file mode 100644 index 000000000..454d2d0a4 --- /dev/null +++ b/cla-backend-go/swagger/common/cla-group-summary.yaml @@ -0,0 +1,86 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +title: CLA Group Summary +description: a summary of the CLA Group information +properties: + foundationLevelCLA: + description: Flag indicating whether CLA is signed at Foundation level (true) or Project level (false) + type: boolean + x-omitempty: false + cla_group_id: + type: string + example: 'b1e86e26-d8c8-4fd8-9f8d-5c723d5dac9f' + description: id of the CLA group + x-omitempty: false + cla_group_name: + $ref: './common/properties/cla-group-name.yaml' + x-omitempty: false + cla_group_description: + $ref: './common/properties/cla-group-description.yaml' + x-omitempty: false + ccla_enabled: + type: boolean + example: true + description: flag to indicate if CCLA is enabled + x-omitempty: false + ccla_requires_icla: + type: boolean + example: true + description: flag to indicate if corporate contributors requires to sign ICLA + x-omitempty: false + icla_enabled: + type: boolean + example: true + description: flag to indicate if ICLA is enabled + x-omitempty: false + template_id: + title: CLA group template + description: the ID of the template - used to generate the ICLA and CCLA PDFs + $ref: './common/properties/internal-id.yaml' + foundation_sfid: + type: string + example: 'a09410000182dD2AAI' + description: foundation sfid under which this CLA group is created + x-omitempty: false + root_project_repositories_count: + type: integer + description: number of repositories added to this CLA Group from root project + x-omitempty: false + foundation_name: + type: string + example: 'Academy Software Foundation' + description: foundation name under which this CLA group is created + x-omitempty: false + repositories_count: + type: integer + description: total repositories under this cla-group + x-omitempty: false + total_signatures: + type: integer + description: aggregate count of ICLA and CCLA contributors within this CLA Group + x-omitempty: false + project_list: + x-omitempty: false + description: list of projects under foundation for which this CLA group is created + type: array + items: + $ref: '#/definitions/cla-group-project' + icla_pdf_url: + description: template URL for ICLA document + type: string + example: 'https://cla-signature-files-dev.s3.amazonaws.com/contract-group/b1e86e26-d8c8-4fd8-9f8d-5c723d5dac9f/template/icla.pdf' + x-omitempty: false + ccla_pdf_url: + description: template URL for CCLA document + type: string + example: 'https://cla-signature-files-dev.s3.amazonaws.com/contract-group/b1e86e26-d8c8-4fd8-9f8d-5c723d5dac9f/template/ccla.pdf' + x-omitempty: false + setup_completion_pct: + description: the CLA Group setup complete percentage, values range from 0-100 inclusive + type: integer + minimum: 0 + maximum: 100 + example: 100 + x-omitempty: false diff --git a/cla-backend-go/swagger/common/cla-group.yaml b/cla-backend-go/swagger/common/cla-group.yaml index 4489f32fd..6f11b52ba 100644 --- a/cla-backend-go/swagger/common/cla-group.yaml +++ b/cla-backend-go/swagger/common/cla-group.yaml @@ -31,12 +31,16 @@ properties: description: Flag indicating whether CLA is signed at Foundation level (true) or Project level (false) example: true type: boolean - x-omitempty: false + x-omitempty: false projectCCLAEnabled: description: Flag to indicate if the Corporate/Company Contributor License Agreement is enabled example: true type: boolean x-omitempty: false + projectTemplateID: + title: CLA group template + description: the ID of the template - used to generate the ICLA and CCLA PDFs + $ref: './common/properties/internal-id.yaml' projectICLAEnabled: description: Flag to indicate if the Individual Contributor License Agreement is enabled example: true diff --git a/cla-backend-go/swagger/common/corporate-contributor.yaml b/cla-backend-go/swagger/common/corporate-contributor.yaml index fc4a5f489..24e35b46c 100644 --- a/cla-backend-go/swagger/common/corporate-contributor.yaml +++ b/cla-backend-go/swagger/common/corporate-contributor.yaml @@ -3,6 +3,10 @@ type: object properties: + signatureID: + description: internal signature ID + $ref: './common/properties/internal-id.yaml' + x-omitempty: false name: type: string example: "john doe" @@ -36,5 +40,11 @@ properties: type: string description: the signature modified created time example: '2019-05-03T18:59:13.082304+0000' - - + signatureSigned: + type: boolean + description: the flag for contributor that has been able to sign + x-omitempty: false + signatureApproved: + type: boolean + description: the flag for contributor that has not yet been approved + x-omitempty: false diff --git a/cla-backend-go/swagger/common/corporate-contributors-list.yaml b/cla-backend-go/swagger/common/corporate-contributors-list.yaml index 5212d7e3b..93d0e2e79 100644 --- a/cla-backend-go/swagger/common/corporate-contributors-list.yaml +++ b/cla-backend-go/swagger/common/corporate-contributors-list.yaml @@ -4,6 +4,18 @@ type: object title: corporate contributors list properties: + nextKey: + type: string + title: Next Key + description: the next key to provide on subsequent API calls to fetch the next page of records + resultCount: + type: integer + format: int64 + x-omitempty: false + totalCount: + type: integer + format: int64 + x-omitempty: false list: type: array items: diff --git a/cla-backend-go/swagger/common/corporate-signature.yaml b/cla-backend-go/swagger/common/corporate-signature.yaml new file mode 100644 index 000000000..0e9db3594 --- /dev/null +++ b/cla-backend-go/swagger/common/corporate-signature.yaml @@ -0,0 +1,188 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +title: A signature model +description: A signature - may be an ICLA or CCLA signature +properties: + signatureID: + description: the signature ID + $ref: './common/properties/internal-id.yaml' + claType: + type: string + description: > + CLA Type field - identifies the specify signature type - individual, employee or corporate signature, valid options: + * `icla` - for individual contributor signature records (individuals not associated with a corporation) + * `ecla` - for employee contributor signature records (acknowledgements from corporate contributors) + * `ccla` - for corporate contributor signature records (created by CLA Signatories and managed by CLA Managers) + enum: [ icla,ecla,ccla ] + signatureCreated: + type: string + description: the signature record created time + example: '2019-05-03T18:59:13.082304+0000' + minLength: 18 + maxLength: 64 + signatureModified: + type: string + description: the signature modified created time + example: '2019-05-03T18:59:13.082304+0000' + minLength: 18 + maxLength: 64 + signatureSigned: + type: boolean + description: the signature signed flag - true or false value + example: true + x-omitempty: false + signatureApproved: + type: boolean + description: the signature approved flag - true or false value + example: true + x-omitempty: false + signatureReferenceType: + type: string + description: the signature reference type - either user or company + example: 'user' + minLength: 2 + maxLength: 12 + signatureReferenceID: + description: the signature reference ID which references a compnay ID or user ID + $ref: './common/properties/internal-id.yaml' + signatureReferenceName: + type: string + signatureReferenceNameLower: + type: string + signatureType: + type: string + description: the signature type - either cla or ccla + example: 'ccla' + minLength: 2 + maxLength: 12 + signedOn: + type: string + signatoryName: + type: string + signatureACL: + type: array + items: + $ref: '#/definitions/user' + userName: + type: string + companyName: + $ref: './common/properties/company-name.yaml' + signingEntityName: + $ref: './common/properties/company-signing-entity-name.yaml' + projectID: + type: string + description: the CLA Group ID + userGHID: + type: string + description: the user's GitHub ID, when available + example: '13434323' + userGHUsername: + type: string + description: the user's GitHub username, when available + example: 'github-user' + userGitlabID: + type: string + description: the user's Gitlab ID, when available + example: '1864' + userGitlabUsername: + type: string + description: the user's Gitlab username, when available + example: 'gitlab-user' + userLFID: + type: string + description: the user's LF Login ID + example: abc1234 + version: + type: string + description: the version of the signature record + example: v1 + minLength: 2 + maxLength: 12 + created: + type: string + description: the date/time when this signature record was created + example: '2017-04-19T16:42:00.000000+0000' + modified: + type: string + description: the date/time when this signature record was last modified + example: '2019-07-15T15:28:33.127118+0000' + signatureMajorVersion: + type: string + description: the signature major version number + example: '2' + signatureMinorVersion: + type: string + description: the signature minor version number + example: '1' + signatureDocumentMajorVersion: + type: string + description: the signature documentt major version + signatureDocumentMinorVersion: + type: string + description: the signature document minor version + signatureSignURL: + type: string + description: the signature Document Sign URL + sigTypeSignedApprovedId: + type: string + signatureCallbackURL: + type: string + description: the signature callback URL + signatureReturnURL: + type: string + description: the signature return URL + signatureReturnURLType: + type: string + description: the signature return URL type + signatureEnvelopeId: + type: string + description: the signature envelope ID + emailApprovalList: + type: array + description: a list of zero or more email addresses in the approval list + x-nullable: true + items: + $ref: "#/definitions/approval-item" + domainApprovalList: + type: array + description: a list of zero or more domains in the approval list + x-nullable: true + items: + $ref: "#/definitions/approval-item" + githubUsernameApprovalList: + type: array + description: a list of zero or more GitHub user name values in the approval list + x-nullable: true + items: + $ref: "#/definitions/approval-item" + githubOrgApprovalList: + type: array + description: a list of zero or more GitHub organization values in the approval list + x-nullable: true + items: + $ref: "#/definitions/approval-item" + gitlabUsernameApprovalList: + type: array + description: a list of zero or more Gitlab user name values in the approval list + x-nullable: true + items: + $ref: "#/definitions/approval-item" + gitlabOrgApprovalList: + type: array + description: a list of zero or more Gitlab organization values in the approval list + x-nullable: true + items: + $ref: "#/definitions/approval-item" + userDocusignName: + type: string + description: full name used on docusign document + userDocusignDateSigned: + type: string + description: docusign signature date + autoCreateECLA: + type: boolean + description: flag to indicate if the product should automatically create an employee acknowledgement for a given user when the CLA manager adds the user to the email, GitLab username, or GitLab username approval list + example: true + x-omitempty: false diff --git a/cla-backend-go/swagger/common/corporate-signatures.yaml b/cla-backend-go/swagger/common/corporate-signatures.yaml new file mode 100644 index 000000000..b0db7fc26 --- /dev/null +++ b/cla-backend-go/swagger/common/corporate-signatures.yaml @@ -0,0 +1,25 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +x-nullable: false +title: Signatures +description: Signatures +properties: + projectID: + type: string + resultCount: + type: integer + format: int64 + x-omitempty: false + totalCount: + type: integer + format: int64 + x-omitempty: false + lastKeyScanned: + type: string + signatures: + type: array + x-omitempty: false + items: + $ref: '#/definitions/corporate-signature' \ No newline at end of file diff --git a/cla-backend-go/swagger/common/create-github-organization.yaml b/cla-backend-go/swagger/common/create-github-organization.yaml deleted file mode 100644 index 56e32e3ab..000000000 --- a/cla-backend-go/swagger/common/create-github-organization.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright The Linux Foundation and each contributor to CommunityBridge. -# SPDX-License-Identifier: MIT - -type: object -required: - - organizationName -properties: - organizationName: - type: string - description: The GitHub Organization name - example: "kubernetes" - # Pattern aligns with UI and other platform services including Org Service - # \w Any word character (alphanumeric & underscore), dashes, periods - pattern: '^([\w\-\.]+){2,255}$' - minLength: 2 - maxLength: 255 - autoEnabled: - type: boolean - description: Flag to indicate if auto-enabled flag should be enabled. Organizations with auto-enable turned on will automatically include any new repositories to the EasyCLA configuration. - default: false - autoEnabledClaGroupID: - type: string - description: Specifies which Cla group ID to be used when autoEnabled flag in enabled for the Github Organization. If autoEnabled is on this field needs to be set as well. - branchProtectionEnabled: - type: boolean - description: Flag to indicate if this GitHub Organization is configured to automatically setup branch protection on CLA enabled repositories. - default: false diff --git a/cla-backend-go/swagger/common/document-tab.yaml b/cla-backend-go/swagger/common/document-tab.yaml new file mode 100644 index 000000000..7ceabc47d --- /dev/null +++ b/cla-backend-go/swagger/common/document-tab.yaml @@ -0,0 +1,41 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +x-nullable: false +title: Docusign Document Tab +description: Docusign Document Tab +properties: + document_tab_type: + type: string + document_tab_id: + type: string + document_tab_name: + type: string + document_tab_page: + type: integer + document_tab_position_x: + type: integer + document_tab_position_y: + type: integer + document_tab_width: + type: integer + document_tab_height: + type: integer + document_tab_is_locked: + type: boolean + document_tab_is_required: + type: boolean + document_tab_anchor_string: + type: string + document_tab_anchor_ignore_if_not_present: + type: boolean + document_tab_anchor_x_offset: + type: integer + document_tab_anchor_y_offset: + type: integer + + + + + diff --git a/cla-backend-go/swagger/common/event-list.yaml b/cla-backend-go/swagger/common/event-list.yaml index 5a2ced159..364abef39 100644 --- a/cla-backend-go/swagger/common/event-list.yaml +++ b/cla-backend-go/swagger/common/event-list.yaml @@ -5,6 +5,8 @@ type: object properties: NextKey: type: string + ResultCount: + type: integer Events: type: array items: diff --git a/cla-backend-go/swagger/common/event.yaml b/cla-backend-go/swagger/common/event.yaml index cb8a03854..a529b0241 100644 --- a/cla-backend-go/swagger/common/event.yaml +++ b/cla-backend-go/swagger/common/event.yaml @@ -4,61 +4,73 @@ type: object properties: EventID: - type: string description: unique id of the event + $ref: './common/properties/internal-id.yaml' EventType: type: string description: type of the event + UserID: - type: string description: id of the user who created this event + $ref: './common/properties/internal-id.yaml' UserName: - type: string - description: name of the user + $ref: './common/properties/user-name.yaml' LfUsername: type: string description: name of the user + + EventCLAGroupID: + description: the CLA Group ID + $ref: './common/properties/internal-id.yaml' + EventCLAGroupName: + description: the CLA Group name + $ref: './common/properties/cla-group-name.yaml' + EventCLAGroupNameLower: + description: the CLA Group name lowercase + $ref: './common/properties/cla-group-name.yaml' + EventProjectID: type: string description: id of the SFID project + EventProjectSFID: + description: the project ID associated with the project. This would be projectSFID if the CLA group have only one project otherwise it would be foundationSFID + $ref: './common/properties/external-id.yaml' + EventProjectSFName: + $ref: './common/properties/project-name.yaml' + description: name of project to display. This would be name of project if cla group have only one project otherwise it would be name of foundation EventProjectName: - type: string - description: name of the project - EventCompanyName: - type: string - description: name of the company - pattern: '^([\w\p{L}][\w\s\p{L}()\[\]+\-/%!@#$]*){2,255}$' - example: "Linux Foundation" + $ref: './common/properties/project-name.yaml' + description: name of project to display. This would be name of project if cla group have only one project otherwise it would be name of foundation + EventParentProjectSFID: + description: the parent project ID associated with the event + $ref: './common/properties/external-id.yaml' + EventParentProjectName: + description: the parent project name associated with the event + $ref: './common/properties/project-name.yaml' + EventCompanyID: type: string description: id of the organization/company + EventCompanySFID: + type: string + description: the external SFID associated with the company + EventCompanyName: + $ref: './common/properties/company-name.yaml' + EventTime: type: string description: time of the event. EventTimeEpoch: type: integer description: time of the event in epoch. + EventData: type: string description: data related to the event EventSummary: type: string description: data related to the event summary - EventProjectExternalID: - type: string - description: the external Project ID related to this event + ContainsPII: type: boolean description: flag to indicate if this record contains personal identifiable information - EventCompanySFID: - type: string - description: the external SFID associated with the company - EventFoundationSFID: - type: string - description: the external SFID associated with the foundation - EventProjectSFID: - type: string - description: the external SFID associated with the project. This would be projectSFID if the CLA group have only one project otherwise it would be foundationSFID - EventProjectSFName: - type: string - description: name of project to display. This would be name of project if cla group have only one project otherwise it would be name of foundation diff --git a/cla-backend-go/swagger/common/gerrit-group-response.yaml b/cla-backend-go/swagger/common/gerrit-group-response.yaml new file mode 100644 index 000000000..da4fac5d6 --- /dev/null +++ b/cla-backend-go/swagger/common/gerrit-group-response.yaml @@ -0,0 +1,38 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +properties: + title: + type: string + title: gerrit group title + description: the gerrit group title + nid: + type: string + 'type': + type: string + title: gerrit type + description: the gerrit type + members: + type: array + items: + type: object + properties: + mail: + type: string + description: the name member mail address + example: 'apache+servicesreleng@mail.linuxfoundation.org' + minLength: 2 + maxLength: 255 + uid: + type: string + description: the member id + example: '255863' + minLength: 2 + maxLength: 255 + username: + type: string + description: the member username + example: 'lfservices_releng' + minLength: 2 + maxLength: 255 diff --git a/cla-backend-go/swagger/common/gerrit-user-list.yaml b/cla-backend-go/swagger/common/gerrit-user-list.yaml new file mode 100644 index 000000000..fb5fd9f11 --- /dev/null +++ b/cla-backend-go/swagger/common/gerrit-user-list.yaml @@ -0,0 +1,10 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: array +items: + type: string + description: the user's user name + example: 'elonmusk' + minLength: 1 + maxLength: 50 diff --git a/cla-backend-go/swagger/common/gerrit.yaml b/cla-backend-go/swagger/common/gerrit.yaml index e7119c2b0..e3d738c44 100644 --- a/cla-backend-go/swagger/common/gerrit.yaml +++ b/cla-backend-go/swagger/common/gerrit.yaml @@ -35,28 +35,11 @@ properties: format: uri groupIdCcla: type: string - description: the LDAP group ID for CCLA + description: the LDAP group ID for CCLA encoded as a string value example: '1902' - minLength: 3 - maxLength: 12 - groupIdIcla: - type: string - description: the LDAP group ID for ICLA - example: '1903' - minLength: 3 + minLength: 1 maxLength: 12 - groupNameCcla: - type: string - description: the LDAP group name for CCLA - example: 'onap-cla-ccla' - minLength: 3 - maxLength: 20 - groupNameIcla: - type: string - description: the LDAP group name for ICLA - example: 'onap-cla-icla' - minLength: 3 - maxLength: 20 + pattern: ^[1-9]\d{0,11}$ projectSFID: type: string description: the Project SalesForce ID (external ID) associated with this gerrit record diff --git a/cla-backend-go/swagger/common/github-organization-create.yaml b/cla-backend-go/swagger/common/github-organization-create.yaml new file mode 100644 index 000000000..1432b16ad --- /dev/null +++ b/cla-backend-go/swagger/common/github-organization-create.yaml @@ -0,0 +1,27 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +required: + - organizationName +properties: + organizationName: + type: string + description: The Organization name + example: "kubernetes" + # Pattern aligns with UI and other platform services including Org Service + # \w Any word character (alphanumeric & underscore), dashes, periods + pattern: '^([\w\-\.]+){2,255}$' + minLength: 2 + maxLength: 255 + autoEnabled: + type: boolean + description: Flag to indicate if auto-enabled flag should be enabled. Organizations with auto-enable turned on will automatically include any new repositories to the EasyCLA configuration. + default: false + autoEnabledClaGroupID: + type: string + description: Specifies which Cla group ID to be used when autoEnabled flag in enabled for the Github Organization. If autoEnabled is on this field needs to be set as well. + branchProtectionEnabled: + type: boolean + description: Flag to indicate if this Organization is configured to automatically setup branch protection on CLA enabled repositories. + default: false diff --git a/cla-backend-go/swagger/common/github-organization-update.yaml b/cla-backend-go/swagger/common/github-organization-update.yaml new file mode 100644 index 000000000..440297615 --- /dev/null +++ b/cla-backend-go/swagger/common/github-organization-update.yaml @@ -0,0 +1,17 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +required: + - autoEnabled +properties: + autoEnabled: + type: boolean + description: Flag to indicate if auto-enabled flag should be enabled. Organizations with auto-enable turned on will automatically include any new repositories to the EasyCLA configuration. + autoEnabledClaGroupID: + type: string + description: Specifies which Cla group ID to be used when autoEnabled flag in enabled for the Github Organization. If autoEnabled is on this field needs to be set as well. + branchProtectionEnabled: + type: boolean + description: Flag to indicate if this Organization is configured to automatically setup branch protection on CLA enabled repositories. + x-omitempty: true diff --git a/cla-backend-go/swagger/common/github-organization.yaml b/cla-backend-go/swagger/common/github-organization.yaml index ee85820e0..d66f4d5b0 100644 --- a/cla-backend-go/swagger/common/github-organization.yaml +++ b/cla-backend-go/swagger/common/github-organization.yaml @@ -28,6 +28,10 @@ properties: projectSFID: type: string example: "a0941000002wBz4AAA" + enabled: + type: boolean + description: Flag that indicates whether this Github Organization is active + x-omitempty: false autoEnabled: type: boolean description: Flag to indicate if this GitHub Organization is configured to allow new repositories to be auto-enabled/auto-enrolled in EasyCLA. @@ -60,6 +64,12 @@ properties: x-nullable: true example: "https://github.com/communitybridge" format: uri + installationURL: + type: string + x-nullable: true + example: "https://github.com/organizations/deal-test-org-2/settings/installations/1235464" + format: uri + repositories: type: object properties: diff --git a/cla-backend-go/swagger/common/list-github-repositories.yaml b/cla-backend-go/swagger/common/github-repositories-list.yaml similarity index 100% rename from cla-backend-go/swagger/common/list-github-repositories.yaml rename to cla-backend-go/swagger/common/github-repositories-list.yaml diff --git a/cla-backend-go/swagger/common/github-repository-input.yaml b/cla-backend-go/swagger/common/github-repository-input.yaml index 3f421679e..c861af306 100644 --- a/cla-backend-go/swagger/common/github-repository-input.yaml +++ b/cla-backend-go/swagger/common/github-repository-input.yaml @@ -12,13 +12,31 @@ required: properties: repositoryExternalID: type: string + description: the repository external identifier, such as the GitHub ID of the repo + example: '337730995' repositoryName: type: string + description: the repository name + example: 'cncf/landscape' repositoryOrganizationName: type: string + description: the repository organization + example: 'cncf' repositoryProjectID: - type: string + description: CLA Group ID + $ref: './common/properties/internal-id.yaml' repositoryType: type: string + description: the repository type + example: 'github' repositoryUrl: type: string + description: the repository URL + example: 'https://github.com/cncf/landscape' + enabled: + type: boolean + description: 'the enabled flag: true or false Repositories can be disabled from CLA to prevent CLA checks' + default: true + note: + description: optional note added to the record + type: string diff --git a/cla-backend-go/swagger/common/github-repository.yaml b/cla-backend-go/swagger/common/github-repository.yaml index 9aebb8eb5..ac114d37f 100644 --- a/cla-backend-go/swagger/common/github-repository.yaml +++ b/cla-backend-go/swagger/common/github-repository.yaml @@ -3,45 +3,66 @@ type: object properties: - dateCreated: - type: string - description: Created date/time - dateModified: - type: string - description: Last modified date/time - repositoryExternalID: - type: string - description: The repository ID from the external service - repositoryID: - type: string + repository_id: description: The internal repository ID - repositoryName: + $ref: './common/properties/internal-id.yaml' + repository_external_id: + type: integer + description: The repository ID from the external service, such as GitHub or GitLab + minimum: 1 + example: 7 + repository_project_sfid: + description: Project SFID + $ref: './common/properties/external-id.yaml' + repository_sfdc_id: + description: Parent Project SFID + $ref: './common/properties/external-id.yaml' + repository_cla_group_id: + description: CLA Group ID + $ref: './common/properties/internal-id.yaml' + repository_name: type: string description: The repository name - repositoryOrganizationName: + example: 'easycla-test-repo-4' + repository_organization_name: type: string description: The organization name associated with this repository - repositoryProjectID: - type: string - description: The CLA Group ID associated with this repository - repositorySfdcID: - type: string - repositoryType: - type: string - description: The repository type - typically github, gerrit or possibly gitlab - repositoryUrl: + example: 'The Linux Foundation/product/EasyCLA' + repository_url: type: string description: The external repository URL + example: 'https://gitlab.com/linuxfoundation/product/easycla/easycla-test-repo-4' + repository_type: + type: string + description: the repository type + example: 'gitlab' enabled: type: boolean - description: Flag to indicate if this repository is enabled or not. Repositories may become disabled if they have been moved or deleted from GitHub. + description: Flag to indicate if this repository is enabled or not. Repositories may become disabled if they have been moved or deleted from GitHub or GitLab. x-omitempty: false + date_created: + type: string + example: "2020-02-06T09:31:49.245630+0000" + minLength: 18 + maxLength: 64 + date_modified: + type: string + example: "2020-02-06T09:31:49.245646+0000" + minLength: 18 + maxLength: 64 note: type: string description: An optional note field to store any additional information about this record. Helpful for auditing. + example: 'optional note about the repository - migrated on MM/DD/YYYY' version: type: string description: The version identifier for this repository record - projectSFID: - type: string - description: The project SFID associated with this repository + example: 'v1' + is_remote_deleted: + type: boolean + description: Is Transferred is a flag to identify, is the repository is transferred, currently not active with current organization + x-omitempty: false + was_cla_enforced: + type: boolean + description: Was CLA Enforced is a flag to identify that, repository was CLA Enforced before transferred. If it was, then it should be enabled with new organization + x-omitempty: false \ No newline at end of file diff --git a/cla-backend-go/swagger/common/gitlab-group-member.yaml b/cla-backend-go/swagger/common/gitlab-group-member.yaml new file mode 100644 index 000000000..3b49d2f81 --- /dev/null +++ b/cla-backend-go/swagger/common/gitlab-group-member.yaml @@ -0,0 +1,36 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +properties: + id: + type: string + description: user id of the gitlab member + username: + type: string + description: username of the gitlab member + name: + type: string + description: name of the gitlab member + state: + type: string + description: state of the gitlab member + avatar_url: + type: string + description: avatar_url of the gitlab member + web_url: + type: string + description: web_url of gitlab member + expired_at: + type: string + description: user expiry time + created_at: + type: string + description: created_at user date + access_level: + type: string + description: access level for user + group_saml_identity: + type: string + description: group saml identity + \ No newline at end of file diff --git a/cla-backend-go/swagger/common/gitlab-group-members-list.yaml b/cla-backend-go/swagger/common/gitlab-group-members-list.yaml new file mode 100644 index 000000000..fd2a7530f --- /dev/null +++ b/cla-backend-go/swagger/common/gitlab-group-members-list.yaml @@ -0,0 +1,9 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +properties: + list: + type: array + items: + $ref: '#/definitions/gitlab-group-member' \ No newline at end of file diff --git a/cla-backend-go/swagger/common/gitlab-organization-create.yaml b/cla-backend-go/swagger/common/gitlab-organization-create.yaml new file mode 100644 index 000000000..fdadba9a0 --- /dev/null +++ b/cla-backend-go/swagger/common/gitlab-organization-create.yaml @@ -0,0 +1,35 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +properties: + # organization_name: + # type: string + # description: The GitLab Group/Organization name + # example: "kubernetes" + # # Pattern aligns with UI and other platform services including Org Service + # # \w Any word character (alphanumeric & underscore), dashes, periods + # pattern: '^([\w\-\.]+){2,255}$' + # minLength: 2 + # maxLength: 255 + group_id: + type: integer + description: The GitLab Group ID + example: 13050017 + minimum: 1 + organization_full_path: + type: string + description: The GitLab Group/Organization full path + example: 'linuxfoundation/product/easycla' + minLength: 3 + auto_enabled: + type: boolean + description: Flag to indicate if auto-enabled flag should be enabled. Organizations with auto-enable turned on will automatically include any new repositories to the EasyCLA configuration. + default: false + auto_enabled_cla_group_id: + $ref: './common/properties/internal-id.yaml' + description: Specifies which CLA Group ID to be used when the auto enabled flag in enabled for the GitLab Group/Organization. When the auto enabled flag is set to true, this field needs to be set to a valid CLA Group ID value. + branch_protection_enabled: + type: boolean + description: Flag to indicate if this GitLab Group/Organization is configured to automatically setup branch protection on CLA enabled repositories. + default: false diff --git a/cla-backend-go/swagger/common/gitlab-organization-update.yaml b/cla-backend-go/swagger/common/gitlab-organization-update.yaml new file mode 100644 index 000000000..fcad44a80 --- /dev/null +++ b/cla-backend-go/swagger/common/gitlab-organization-update.yaml @@ -0,0 +1,16 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +description: GitLab Organization Update model +properties: + auto_enabled: + type: boolean + description: Flag to indicate if auto-enabled flag should be enabled. Group/Organizations with auto-enable turned on will automatically include any new repositories to the EasyCLA configuration. + auto_enabled_cla_group_id: + $ref: './common/properties/internal-id.yaml' + description: Specifies which CLA Group ID to be used when the auto enabled flag in enabled for the GitLab Group/Organization. When the auto enabled flag is set to true, this field needs to be set to a valid CLA Group ID value. + branch_protection_enabled: + type: boolean + description: Flag to indicate if this Group/Organization is configured to automatically setup branch protection on CLA enabled repositories. + x-omitempty: true diff --git a/cla-backend-go/swagger/common/gitlab-organization.yaml b/cla-backend-go/swagger/common/gitlab-organization.yaml new file mode 100644 index 000000000..36e5a329a --- /dev/null +++ b/cla-backend-go/swagger/common/gitlab-organization.yaml @@ -0,0 +1,107 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +properties: + organization_id: + type: string + description: internal id of the gitlab organization + organization_external_id: + type: integer + description: The Gitlab Group/Organization external ID used by GitLab + example: 13050017 + minimum: 1 + date_created: + type: string + example: "2020-02-06T09:31:49.245630+0000" + minLength: 18 + maxLength: 64 + date_modified: + type: string + example: "2020-02-06T09:31:49.245646+0000" + minLength: 18 + maxLength: 64 + organization_name: + type: string + example: "communitybridge" + organization_url: + type: string + description: The Gitlab Group/Organization url + example: "github.com/Linux Foundation/product/EasyCLA" + organization_full_path: + type: string + description: The Gitlab Group/Organization full path + example: "linuxfoundation/product/easycla" + organization_sfid: + type: string + example: "a0941000002wBz4AAA" + version: + type: string + example: "v1" + project_sfid: + type: string + example: "a0941000002wBz4AAA" + enabled: + type: boolean + description: Flag that indicates whether this Gitlab Organization is active + x-omitempty: false + connected: + type: boolean + description: Flag that indicates whether this Gitlab Organization is authorized with Gitlab, if false it might mean that Gitlab Oauth process is not compeleted yet or the token was revoked and user needs to go through the auth process again + x-omitempty: false + auto_enabled: + type: boolean + description: Flag to indicate if this Gitlab Organization is configured to allow new repositories to be auto-enabled/auto-enrolled in EasyCLA. + x-omitempty: false + auto_enabled_cla_group_id: + type: string + description: Specifies which Cla group ID to be used when autoEnabled flag in enabled for the Github Organization. If autoEnabled is on this field needs to be set as well. + branch_protection_enabled: + type: boolean + description: Flag to indicate if this GitHub Organization is configured to automatically setup branch protection on CLA enabled repositories. + x-omitempty: false + auth_info: + type: string + description: auth info + auth_state: + type: string + description: auth state + auth_expiry_time: + type: integer + description: auth expiry time + gitlab_info: + type: object + properties: + error: + type: string + example: "unable to get gitlab info of communitybridge" + details: + type: object + properties: + id: + type: integer + x-nullable: true + example: 1476068 + bio: + type: string + x-nullable: true + html_url: + type: string + x-nullable: true + example: "https://github.com/communitybridge" + format: uri + installation_url: + type: string + x-nullable: true + description: "if the Gitlab Organization is not connected yet can use this url to go through the process of authorizing the easyCLA bot" + format: uri + repositories: + type: object + properties: + error: + type: string + example: "unable to get repositories for installation id : 6854001" + list: + type: array + items: + $ref: '#/definitions/gitlab-repository-info' diff --git a/cla-backend-go/swagger/common/gitlab-organizations.yaml b/cla-backend-go/swagger/common/gitlab-organizations.yaml new file mode 100644 index 000000000..52aafb588 --- /dev/null +++ b/cla-backend-go/swagger/common/gitlab-organizations.yaml @@ -0,0 +1,10 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +description: GitLab Organizations +properties: + list: + type: array + items: + $ref: '#/definitions/gitlab-organization' diff --git a/cla-backend-go/swagger/common/gitlab-project-organization.yaml b/cla-backend-go/swagger/common/gitlab-project-organization.yaml new file mode 100644 index 000000000..be9d40e84 --- /dev/null +++ b/cla-backend-go/swagger/common/gitlab-project-organization.yaml @@ -0,0 +1,68 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +description: GitLab Project Organization +properties: + project_sfid: + description: The project SFID + $ref: './common/properties/external-id.yaml' + parent_project_sfid: + description: The parent project SFID + $ref: './common/properties/external-id.yaml' + auto_enabled: + type: boolean + description: Flag to indicate if auto-enabled flag should be enabled. Organizations with auto-enable turned on will automatically include any new repositories to the EasyCLA configuration. + x-omitempty: false + auto_enable_cla_group_id: + type: string + description: The CLA Group ID which is attached to the auto-enabled flag + auto_enabled_cla_group_name: + type: string + description: The CLA Group name which is attached to the auto-enabled flag + branch_protection_enabled: + type: boolean + description: Flag to indicate if this GitHub Organization is configured to automatically setup branch protection on CLA enabled repositories. + x-omitempty: false + installation_url: + type: string + x-nullable: true + format: uri + organization_name: + type: string + description: The Gitlab Organization name + example: "kubernetes" + # Pattern aligns with UI and other platform services including Org Service + # \w Any word character (alphanumeric & underscore), dashes, periods + pattern: '^([\w\-\.]+){2,255}$' + minLength: 2 + maxLength: 255 + organization_url: + type: string + description: The Gitlab Group/Organization url + example: "github.com/Linux Foundation/product/EasyCLA" + organization_full_path: + type: string + description: The Gitlab Group/Organization full path + example: "linuxfoundation/product/easycla" + organization_external_id: + type: integer + description: The Gitlab Group/Organization external ID used by GitLab + example: 13050017 + minimum: 1 + connection_status: + type: string + enum: + - connected + - partial_connection + - connection_failure + - no_connection + connection_status_message: + type: string + description: An optional connection status message + example: 'Token was revoked. You have to re-authorize from the user.' + x-omitempty: true + repositories: + type: array + items: + $ref: '#/definitions/gitlab-project-repository' diff --git a/cla-backend-go/swagger/common/gitlab-project-organizations.yaml b/cla-backend-go/swagger/common/gitlab-project-organizations.yaml new file mode 100644 index 000000000..152611ecd --- /dev/null +++ b/cla-backend-go/swagger/common/gitlab-project-organizations.yaml @@ -0,0 +1,10 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +description: GitLab Project Organizations +properties: + list: + type: array + items: + $ref: '#/definitions/gitlab-project-organization' diff --git a/cla-backend-go/swagger/common/gitlab-project-repository.yaml b/cla-backend-go/swagger/common/gitlab-project-repository.yaml new file mode 100644 index 000000000..d5828ce47 --- /dev/null +++ b/cla-backend-go/swagger/common/gitlab-project-repository.yaml @@ -0,0 +1,54 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +description: GitLab Project Repository +properties: + repository_id: + description: Repository Internal ID + $ref: './common/properties/internal-id.yaml' + x-omitempty: false + repository_gitlab_id: + type: integer + description: 'Repository GitLab ID value' + minimum: 1 + example: 2292 + repository_name: + type: string + description: 'GitLab Repository/Project name' + example: 'easycla-test-repo-4' + x-omitempty: false + repository_full_path: + type: string + description: The repository full path + example: 'linuxfoundation/product/easycla/easycla-test-repo-4' + minLength: 3 + x-omitempty: false + repository_url: + type: string + description: 'GitLab Repository/Project URL' + minLength: 8 + example: 'https://gitlab.com/linuxfoundation/product/easycla/easycla-test-repo-4' + x-omitempty: false + cla_group_id: + description: CLA Group ID + $ref: './common/properties/internal-id.yaml' + x-omitempty: false + project_id: + description: Project SFID + $ref: './common/properties/external-id.yaml' + x-omitempty: false + parent_project_id: + description: Parent Project SFID + $ref: './common/properties/external-id.yaml' + x-omitempty: false + enabled: + type: boolean + description: 'Enabled flag' + x-omitempty: false + connection_status: + type: string + description: 'Connection status for the repository, one of the supported values connected or connection_failure' + enum: + - connected + - connection_failure diff --git a/cla-backend-go/swagger/common/gitlab-repositories-enroll.yaml b/cla-backend-go/swagger/common/gitlab-repositories-enroll.yaml new file mode 100644 index 000000000..6c491a657 --- /dev/null +++ b/cla-backend-go/swagger/common/gitlab-repositories-enroll.yaml @@ -0,0 +1,25 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +description: 'GitLab repositories enable model' +properties: + cla_group_id: + description: CLA Group ID + $ref: './common/properties/internal-id.yaml' + enroll: + type: array + description: a list of GitLab repositories to enroll + items: + description: the GitLab repository external identifier, such as the GitLab ID of the repository + type: integer + minimum: 1 + example: 7 + unenroll: + type: array + description: a list of GitLab repositories to unenroll + items: + description: the GitLab repository external identifier, such as the GitLab ID of the repository + type: integer + minimum: 1 + example: 7 diff --git a/cla-backend-go/swagger/common/gitlab-repositories-list.yaml b/cla-backend-go/swagger/common/gitlab-repositories-list.yaml new file mode 100644 index 000000000..1e9440adc --- /dev/null +++ b/cla-backend-go/swagger/common/gitlab-repositories-list.yaml @@ -0,0 +1,9 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +properties: + list: + type: array + items: + $ref: '#/definitions/gitlab-repository' diff --git a/cla-backend-go/swagger/common/gitlab-repository-info.yaml b/cla-backend-go/swagger/common/gitlab-repository-info.yaml new file mode 100644 index 000000000..3bf9699b1 --- /dev/null +++ b/cla-backend-go/swagger/common/gitlab-repository-info.yaml @@ -0,0 +1,28 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +properties: + repository_gitlab_id: + type: integer + description: 'Repository GitLab ID value' + minimum: 1 + example: 2292 + repository_name: + type: string + description: 'GitLab Repository/Project name' + example: 'easycla-test-repo-4' + minLength: 3 + repository_type: + type: string + example: "github" + repository_url: + type: string + description: 'GitLab Repository/Project URL' + minLength: 8 + example: 'https://gitlab.com/linuxfoundation/product/easycla/easycla-test-repo-4' + repository_full_path: + type: string + description: The repository full path + example: 'linuxfoundation/product/easycla/easycla-test-repo-4' + minLength: 3 diff --git a/cla-backend-go/swagger/common/gitlab-repository.yaml b/cla-backend-go/swagger/common/gitlab-repository.yaml new file mode 100644 index 000000000..8628be883 --- /dev/null +++ b/cla-backend-go/swagger/common/gitlab-repository.yaml @@ -0,0 +1,61 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +properties: + repositoryID: + description: The internal repository ID + $ref: './common/properties/internal-id.yaml' + repository_external_id: + type: integer + description: The repository ID from the external service, such as GitHub or GitLab + minimum: 1 + example: 7 + repository_cla_group_id: + type: string + description: The CLA Group ID associated with this repository + repository_project_sfid: + description: Project SFID + $ref: './common/properties/external-id.yaml' + repository_name: + type: string + description: The repository name + example: 'easycla-test-repo-4' + repository_full_path: + type: string + description: The repository full path + example: 'linuxfoundation/product/easycla/easycla-test-repo-4' + repository_organization_name: + type: string + description: The organization name associated with this repository + example: 'The Linux Foundation/product/EasyCLA' + repository_url: + type: string + description: The external repository URL + example: 'https://gitlab.com/linuxfoundation/product/easycla/easycla-test-repo-4' + repository_type: + type: string + description: the repository type + example: 'gitlab' + enabled: + type: boolean + description: Flag to indicate if this repository is enabled or not. Repositories may become disabled if they have been moved or deleted from GitHub or GitLab. + x-omitempty: false + date_created: + type: string + example: "2020-02-06T09:31:49.245630+0000" + minLength: 18 + maxLength: 64 + date_modified: + type: string + example: "2020-02-06T09:31:49.245646+0000" + minLength: 18 + maxLength: 64 + note: + type: string + description: An optional note field to store any additional information about this record. Helpful for auditing. + example: 'optional note about the repository - migrated on MM/DD/YYYY' + version: + type: string + description: The version identifier for this repository record + example: 'v1' diff --git a/cla-backend-go/swagger/common/icla-signature.yaml b/cla-backend-go/swagger/common/icla-signature.yaml index 41e085b4f..39662700c 100644 --- a/cla-backend-go/swagger/common/icla-signature.yaml +++ b/cla-backend-go/swagger/common/icla-signature.yaml @@ -5,21 +5,53 @@ type: object title: ICLASignature properties: signature_id: - type: string + description: the signature ID + $ref: './common/properties/internal-id.yaml' + user_id: + description: the user ID + $ref: './common/properties/internal-id.yaml' github_username: type: string + description: the user's github username + example: 'tomcruise' + gitlab_username: + type: string + description: the user's gitlab username + example: 'tomcruise' lf_username: type: string + description: the user's LF username + example: 'tomcruise' user_name: type: string + description: the user's user name/real name + example: 'Tom Cruise' user_email: type: string + description: the user's email address + example: 'tomcruise@hollywood.com' signed_on: type: string + description: the date/time the ICLA was signed + example: '2020-03-16T17:57:58Z' userDocusignName: type: string + description: the name provided on the DocuSign document + example: 'Jack Daniels' userDocusignDateSigned: type: string + description: the signature date signed value from DocuSign + example: '2020-03-16T17:57:58Z' + signatureApproved: + type: boolean + description: the signature approved flag - true or false value + example: true + x-omitempty: false + signatureSigned: + type: boolean + description: the signature signed flag - true or false value + example: true + x-omitempty: false signatureModified: type: string description: the signature modified created time diff --git a/cla-backend-go/swagger/common/icla-signatures.yaml b/cla-backend-go/swagger/common/icla-signatures.yaml index e337d4a4e..b967732ac 100644 --- a/cla-backend-go/swagger/common/icla-signatures.yaml +++ b/cla-backend-go/swagger/common/icla-signatures.yaml @@ -3,6 +3,14 @@ type: object properties: + lastKeyScanned: + type: string + pageSize: + type: integer + resultCount: + type: integer + format: int64 + x-omitempty: false list: type: array items: diff --git a/cla-backend-go/swagger/common/org.yaml b/cla-backend-go/swagger/common/org.yaml index 3586cc608..fd526f885 100644 --- a/cla-backend-go/swagger/common/org.yaml +++ b/cla-backend-go/swagger/common/org.yaml @@ -15,3 +15,8 @@ properties: organization_website: $ref: './common/properties/website.yaml' description: website of the organization + ccla_enabled: + type: boolean + default: false + description: Status describing if organization has signed CCLA + diff --git a/cla-backend-go/swagger/common/properties/company-signing-entity-name.yaml b/cla-backend-go/swagger/common/properties/company-signing-entity-name.yaml index 1448d6981..6d3497e3f 100644 --- a/cla-backend-go/swagger/common/properties/company-signing-entity-name.yaml +++ b/cla-backend-go/swagger/common/properties/company-signing-entity-name.yaml @@ -3,7 +3,7 @@ example: "Linux Foundation" type: string -description: Name of the company +description: Signing Entity Name of the Company # Pattern aligns with UI and other platform services including Org Service pattern: '[^<>]*' # allow everything except greater than and less than symbols minLength: 2 diff --git a/cla-backend-go/swagger/common/properties/email.yaml b/cla-backend-go/swagger/common/properties/email.yaml index d6bb5950d..ae624b851 100644 --- a/cla-backend-go/swagger/common/properties/email.yaml +++ b/cla-backend-go/swagger/common/properties/email.yaml @@ -4,4 +4,4 @@ type: string example: "user@linuxfoundation.org" format: email -pattern : '^([a-zA-Z0-9_\-\.\+]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$' +pattern : '^([a-zA-Z0-9_\-\.\+]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,10})$' diff --git a/cla-backend-go/swagger/common/properties/user-name.yaml b/cla-backend-go/swagger/common/properties/user-name.yaml new file mode 100644 index 000000000..a3f1f4819 --- /dev/null +++ b/cla-backend-go/swagger/common/properties/user-name.yaml @@ -0,0 +1,8 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: string +example: "Derk Miyamoto" +pattern: ^[a-zA-Z0-9][a-zA-Z0-9 ',.;:-_&@\#\$\(\)\+]*$ +minLength: 2 +maxLength: 255 diff --git a/cla-backend-go/swagger/common/signature-acl.yaml b/cla-backend-go/swagger/common/signature-acl.yaml new file mode 100644 index 000000000..4d245e56b --- /dev/null +++ b/cla-backend-go/swagger/common/signature-acl.yaml @@ -0,0 +1,10 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +title: signature acl list representing cla managers access list +properties: + username_list: + type: array + items: + type: string diff --git a/cla-backend-go/swagger/common/signature-approval-list.yaml b/cla-backend-go/swagger/common/signature-approval-list.yaml index 1fc372e91..a9f80ad0f 100644 --- a/cla-backend-go/swagger/common/signature-approval-list.yaml +++ b/cla-backend-go/swagger/common/signature-approval-list.yaml @@ -7,50 +7,87 @@ description: A signature approval list for a project / company properties: AddEmailApprovalList: type: array + title: Add User Email description: a list of zero or more email addresses to be added to the approval list x-nullable: true items: type: string RemoveEmailApprovalList: type: array - description: a list of zero or more email addresses to be from to the approval list + title: Remove User Email + description: a list of zero or more email addresses to be removed from the approval list x-nullable: true items: type: string AddDomainApprovalList: type: array + title: Add Domain Email description: a list of zero or more domains to be added to the approval list x-nullable: true items: type: string + example: 'linuxfoundation.org' RemoveDomainApprovalList: type: array + title: Remove Domain Email description: a list of zero or more domains to be removed from the approval list x-nullable: true items: type: string + example: 'linuxfoundation.org' AddGithubUsernameApprovalList: type: array + title: Add GitHub Username description: a list of zero or more GitHub user name values to be added to the approval list x-nullable: true items: type: string RemoveGithubUsernameApprovalList: type: array + title: Remove GitHub Username description: a list of zero or more GitHub user name values to be removed from the approval list x-nullable: true items: type: string AddGithubOrgApprovalList: type: array + title: Add GitHub Organization description: a list of zero or more GitHub organization values to be added to the approval list x-nullable: true items: type: string RemoveGithubOrgApprovalList: type: array + title: Remove GitHub Organization description: a list of zero or more GitHub organization values to be removed from the approval list x-nullable: true items: type: string - + AddGitlabUsernameApprovalList: + type: array + title: Add Gitlab Username + description: a list of zero or more Gitlab user name values to be added to the approval list + x-nullable: true + items: + type: string + RemoveGitlabUsernameApprovalList: + type: array + title: Remove Gitlab Username + description: a list of zero or more Gitlab user name values to be removed from the approval list + x-nullable: true + items: + type: string + AddGitlabOrgApprovalList: + type: array + title: Add Gitlab Organization + description: a list of zero or more Gitlab organization values to be added to the approval list + x-nullable: true + items: + type: string + RemoveGitlabOrgApprovalList: + type: array + title: Remove Gitlab Organization + description: a list of zero or more Gitlab organization values to be removed from the approval list + x-nullable: true + items: + type: string diff --git a/cla-backend-go/swagger/common/signature-report.yaml b/cla-backend-go/swagger/common/signature-report.yaml new file mode 100644 index 000000000..ad3f7d7a8 --- /dev/null +++ b/cla-backend-go/swagger/common/signature-report.yaml @@ -0,0 +1,25 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +x-nullable: false +title: Signature Report +description: Signature Report +properties: + projectID: + type: string + resultCount: + type: integer + format: int64 + x-omitempty: false + totalCount: + type: integer + format: int64 + x-omitempty: false + lastKeyScanned: + type: string + signatures: + type: array + x-omitempty: false + items: + $ref: '#/definitions/signature-summary' diff --git a/cla-backend-go/swagger/common/signature-summary.yaml b/cla-backend-go/swagger/common/signature-summary.yaml new file mode 100644 index 000000000..69290d7bb --- /dev/null +++ b/cla-backend-go/swagger/common/signature-summary.yaml @@ -0,0 +1,64 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +title: A signature summary model +description: A signature summary model +properties: + signatureID: + description: the signature ID for a compnay record + $ref: './common/properties/internal-id.yaml' + projectID: + description: the CLA Group ID + $ref: './common/properties/internal-id.yaml' + claType: + type: string + description: > + CLA Type field - identifies the specify signature type - individual, employee or corporate signature, valid options: + * `icla` - for individual contributor signature records (individuals not associated with a corporation) + * `ecla` - for employee contributor signature records (acknowledgements from corporate contributors) + * `ccla` - for corporate contributor signature records (created by CLA Signatories and managed by CLA Managers) + enum: [ icla,ecla,ccla ] + signatureSigned: + type: boolean + description: the signature signed flag - true or false value + example: true + x-omitempty: false + signatureApproved: + type: boolean + description: the signature approved flag - true or false value + example: true + x-omitempty: false + signatureReferenceType: + type: string + description: the signature reference type - either user or company + example: 'user' + minLength: 2 + maxLength: 12 + signatureReferenceID: + description: the signature reference ID which references a compnay ID or user ID + $ref: './common/properties/internal-id.yaml' + signatureReferenceName: + type: string + signatureReferenceNameLower: + type: string + signatureType: + type: string + description: the signature type - either cla or ccla + example: 'ccla' + minLength: 2 + maxLength: 12 + signedOn: + type: string + signatoryName: + type: string + companyName: + $ref: './common/properties/company-name.yaml' + signingEntityName: + $ref: './common/properties/company-signing-entity-name.yaml' + userDocusignName: + type: string + description: full name used on docusign document + userDocusignDateSigned: + type: string + description: docusign signature date diff --git a/cla-backend-go/swagger/common/signature.yaml b/cla-backend-go/swagger/common/signature.yaml index 27d1e5489..707d36f97 100644 --- a/cla-backend-go/swagger/common/signature.yaml +++ b/cla-backend-go/swagger/common/signature.yaml @@ -6,10 +6,8 @@ title: A signature model description: A signature - may be an ICLA or CCLA signature properties: signatureID: - type: string description: the signature ID - example: 'c71c469a-55ea-492d-9722-fd30b31da2aa' - format: uuid4 + $ref: './common/properties/internal-id.yaml' claType: type: string description: > @@ -34,10 +32,12 @@ properties: type: boolean description: the signature signed flag - true or false value example: true + x-omitempty: false signatureApproved: type: boolean description: the signature approved flag - true or false value example: true + x-omitempty: false signatureReferenceType: type: string description: the signature reference type - either user or company @@ -45,10 +45,8 @@ properties: minLength: 2 maxLength: 12 signatureReferenceID: - type: string description: the signature reference ID which references a compnay ID or user ID - example: 'c71c469a-55ea-492d-9722-fd30b31da2aa' - format: uuid4 + $ref: './common/properties/internal-id.yaml' signatureReferenceName: type: string signatureReferenceNameLower: @@ -70,9 +68,9 @@ properties: userName: type: string companyName: - type: string - description: the company name - pattern: '^([\w\p{L}][\w\s\p{L}()\[\]+\-/%!@#$]*){2,255}$' + $ref: './common/properties/company-name.yaml' + signingEntityName: + $ref: './common/properties/company-signing-entity-name.yaml' projectID: type: string description: the CLA Group ID @@ -83,7 +81,15 @@ properties: userGHUsername: type: string description: the user's GitHub username, when available - example: linux-user + example: 'github-user' + userGitlabID: + type: string + description: the user's Gitlab ID, when available + example: '1864' + userGitlabUsername: + type: string + description: the user's Gitlab username, when available + example: 'gitlab-user' userLFID: type: string description: the user's LF Login ID @@ -110,6 +116,29 @@ properties: type: string description: the signature minor version number example: '1' + signatureDocumentMajorVersion: + type: string + description: the signature documentt major version + signatureDocumentMinorVersion: + type: string + description: the signature document minor version + signatureSignURL: + type: string + description: the signature Document Sign URL + sigTypeSignedApprovedId: + type: string + signatureCallbackURL: + type: string + description: the signature callback URL + signatureReturnURL: + type: string + description: the signature return URL + signatureReturnURLType: + type: string + description: the signature return URL type + signatureEnvelopeId: + type: string + description: the signature envelope ID emailApprovalList: type: array description: a list of zero or more email addresses in the approval list @@ -134,9 +163,26 @@ properties: x-nullable: true items: type: string + gitlabUsernameApprovalList: + type: array + description: a list of zero or more Gitlab user name values in the approval list + x-nullable: true + items: + type: string + gitlabOrgApprovalList: + type: array + description: a list of zero or more Gitlab organization values in the approval list + x-nullable: true + items: + type: string userDocusignName: type: string description: full name used on docusign document userDocusignDateSigned: type: string description: docusign signature date + autoCreateECLA: + type: boolean + description: flag to indicate if the product should automatically create an employee acknowledgement for a given user when the CLA manager adds the user to the email, GitLab username, or GitLab username approval list + example: true + x-omitempty: false diff --git a/cla-backend-go/swagger/common/update-github-organization.yaml b/cla-backend-go/swagger/common/update-github-organization.yaml deleted file mode 100644 index 82ccc4339..000000000 --- a/cla-backend-go/swagger/common/update-github-organization.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright The Linux Foundation and each contributor to CommunityBridge. -# SPDX-License-Identifier: MIT - -type: object -required: - - autoEnabled -properties: - autoEnabled: - type: boolean - description: Flag to indicate if auto-enabled flag should be enabled. Organizations with auto-enable turned on will automatically include any new repositories to the EasyCLA configuration. - autoEnabledClaGroupID: - type: string - description: Specifies which Cla group ID to be used when autoEnabled flag in enabled for the Github Organization. If autoEnabled is on this field needs to be set as well. - branchProtectionEnabled: - type: boolean - description: Flag to indicate if this GitHub Organization is configured to automatically setup branch protection on CLA enabled repositories. - x-omitempty: true diff --git a/cla-backend-go/swagger/common/user.yaml b/cla-backend-go/swagger/common/user.yaml index d4f332b0a..4dfc67960 100644 --- a/cla-backend-go/swagger/common/user.yaml +++ b/cla-backend-go/swagger/common/user.yaml @@ -7,9 +7,11 @@ title: User description: User model properties: userID: - type: string + $ref: './common/properties/internal-id.yaml' + description: the user's internal/unique ID userExternalID: - type: string + $ref: './common/properties/external-id.yaml' + description: the user's external ID tied to SF username: type: string dateCreated: @@ -17,22 +19,41 @@ properties: dateModified: type: string lfEmail: - type: string + $ref: './common/properties/email.yaml' lfUsername: type: string companyID: - type: string + $ref: './common/properties/internal-id.yaml' + description: the user's optional company ID githubID: type: string + description: the user's github ID + example: '123434' githubUsername: type: string + description: the user's github username + example: 'grapes42' + gitlabID: + type: string + description: the user's gitlab ID + example: '123434' + gitlabUsername: + type: string + description: the user's gitlab username + example: 'orangejuice' admin: type: boolean version: type: string + description: the version identifier for this record + example: 'v1' note: type: string + description: an optional note for this user record emails: type: array items: type: string + userCompanyID: + type: string + description: the user's optional company ID diff --git a/cla-backend-go/swagger/requirements.txt b/cla-backend-go/swagger/requirements.txt index 3b2ca554f..2b8cda8f5 100644 --- a/cla-backend-go/swagger/requirements.txt +++ b/cla-backend-go/swagger/requirements.txt @@ -2,4 +2,4 @@ # SPDX-License-Identifier: MIT click==7.1.2 -PyYAML==5.3.1 +PyYAML>=5.4 diff --git a/cla-backend-go/template/handlers.go b/cla-backend-go/template/handlers.go index c4074da60..d01f67b0f 100644 --- a/cla-backend-go/template/handlers.go +++ b/cla-backend-go/template/handlers.go @@ -4,17 +4,22 @@ package template import ( + "context" + "fmt" + "github.com/communitybridge/easycla/cla-backend-go/events" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/template" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/template" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/user" + "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/go-openapi/runtime/middleware" + "github.com/sirupsen/logrus" ) // Configure API call -func Configure(api *operations.ClaAPI, service Service, eventsService events.Service) { +func Configure(api *operations.ClaAPI, service ServiceInterface, eventsService events.Service) { // Retrieve a list of available templates api.TemplateGetTemplatesHandler = template.GetTemplatesHandlerFunc(func(params template.GetTemplatesParams, claUser *user.CLAUser) middleware.Responder { @@ -26,17 +31,47 @@ func Configure(api *operations.ClaAPI, service Service, eventsService events.Ser }) api.TemplateCreateCLAGroupTemplateHandler = template.CreateCLAGroupTemplateHandlerFunc(func(params template.CreateCLAGroupTemplateParams, claUser *user.CLAUser) middleware.Responder { + reqID := utils.GetRequestID(params.XREQUESTID) + ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + f := logrus.Fields{ + "functionName": "v2.signatures.handlers.SignaturesGetProjectSignaturesHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": params.ClaGroupID, + "templateID": params.Body.TemplateID, + } + pdfUrls, err := service.CreateCLAGroupTemplate(params.HTTPRequest.Context(), params.ClaGroupID, ¶ms.Body) if err != nil { - log.Warnf("Error generating PDFs from provided templates, error: %v", err) - return template.NewGetTemplatesBadRequest().WithPayload(errorResponse(err)) + msg := fmt.Sprintf("Error generating PDFs from provided templates, error: %v", err) + log.WithFields(f).WithError(err).Warn(msg) + return template.NewGetTemplatesBadRequest().WithPayload(utils.ToV1ErrorResponse(utils.ErrorResponseBadRequestWithError(reqID, msg, err))) + } + + // Need the template name for the event log + templateName, lookupErr := service.GetTemplateName(ctx, params.Body.TemplateID) + if lookupErr != nil || templateName == "" { + msg := fmt.Sprintf("Error looking up template name with ID: %s", params.Body.TemplateID) + log.WithFields(f).WithError(lookupErr).Warn(msg) + return template.NewGetTemplatesBadRequest().WithPayload(utils.ToV1ErrorResponse(utils.ErrorResponseBadRequestWithError(reqID, msg, lookupErr))) + } + + // Grab the new POC value from the request + newPOCValue := "" + for _, field := range params.Body.MetaFields { + if field.TemplateVariable == "CONTACT_EMAIL" { + newPOCValue = field.Value + break + } } eventsService.LogEvent(&events.LogEventArgs{ EventType: events.CLATemplateCreated, ProjectID: params.ClaGroupID, UserID: claUser.UserID, - EventData: &events.CLATemplateCreatedEventData{}, + EventData: &events.CLATemplateCreatedEventData{ + TemplateName: templateName, + NewPOC: newPOCValue, + }, }) return template.NewCreateCLAGroupTemplateOK().WithPayload(&pdfUrls) diff --git a/cla-backend-go/template/repository.go b/cla-backend-go/template/repository.go index f83d0a28a..574563488 100644 --- a/cla-backend-go/template/repository.go +++ b/cla-backend-go/template/repository.go @@ -20,7 +20,7 @@ import ( "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" ) var ( @@ -35,16 +35,19 @@ var ( ASWFStyleTemplateID = "18b8ad08-d7d4-4d75-ad25-30bbfffd59cf" ) -// Repository interface functions -type Repository interface { +// RepositoryInterface interface functions +type RepositoryInterface interface { GetTemplates(ctx context.Context) ([]models.Template, error) + GetTemplateName(ctx context.Context, templateID string) (string, error) GetTemplate(templateID string) (models.Template, error) + CLAGroupTemplateExists(ctx context.Context, templateID string) bool GetCLAGroup(claGroupID string) (*models.ClaGroup, error) GetCLADocuments(claGroupID string, claType string) ([]models.ClaGroupDocument, error) UpdateDynamoContractGroupTemplates(ctx context.Context, ContractGroupID string, template models.Template, pdfUrls models.TemplatePdfs, projectCCLAEnabled, projectICLAEnabled bool) error } -type repository struct { +// Repository object/struct +type Repository struct { stage string // The AWS stage (dev, staging, prod) dynamoDBClient *dynamodb.DynamoDB } @@ -97,16 +100,16 @@ type DocumentTab struct { DocumentTabAnchorIgnoreIfNotPresent bool `json:"document_tab_anchor_ignore_if_not_present"` } -// NewRepository creates a new instance of the repository service -func NewRepository(awsSession *session.Session, stage string) repository { - return repository{ +// NewRepository creates a new instance of the Repository service +func NewRepository(awsSession *session.Session, stage string) Repository { + return Repository{ stage: stage, dynamoDBClient: dynamodb.New(awsSession), } } // GetTemplates returns a list containing all the template models -func (r repository) GetTemplates(ctx context.Context) ([]models.Template, error) { +func (r Repository) GetTemplates(ctx context.Context) ([]models.Template, error) { f := logrus.Fields{ "functionName": "GetTemplates", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), @@ -127,8 +130,28 @@ func (r repository) GetTemplates(ctx context.Context) ([]models.Template, error) return templates, nil } +// GetTemplateName returns the template name when provided the template ID +func (r Repository) GetTemplateName(ctx context.Context, templateID string) (string, error) { + f := logrus.Fields{ + "functionName": "v1.template.repository.GetTemplateName", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "templateID": templateID, + } + + // For each template... + for _, template := range templateMap { + // If we have a match + if template.ID == templateID { + return template.Name, nil + } + } + + log.WithFields(f).Warnf("unable to locate template with ID: %s", templateID) + return "", nil +} + // GetTemplate returns the template based on the template ID -func (r repository) GetTemplate(templateID string) (models.Template, error) { +func (r Repository) GetTemplate(templateID string) (models.Template, error) { template, ok := templateMap[templateID] if !ok { return models.Template{}, ErrTemplateNotFound @@ -137,10 +160,16 @@ func (r repository) GetTemplate(templateID string) (models.Template, error) { return template, nil } +// CLAGroupTemplateExists return true if the specified template ID exists, false otherwise +func (r Repository) CLAGroupTemplateExists(ctx context.Context, templateID string) bool { + _, ok := templateMap[templateID] + return ok +} + // GetCLAGroup This method belongs in the contract group package. We are leaving it here -// because it accesses DynamoDB, but the contract group repository is designed +// because it accesses DynamoDB, but the contract group Repository is designed // to connect to postgres -func (r repository) GetCLAGroup(claGroupID string) (*models.ClaGroup, error) { +func (r Repository) GetCLAGroup(claGroupID string) (*models.ClaGroup, error) { log.Debugf("GetCLAGroup - claGroupID: %s", claGroupID) dbModel, err := r.fetchCLAGroup(claGroupID) @@ -151,7 +180,7 @@ func (r repository) GetCLAGroup(claGroupID string) (*models.ClaGroup, error) { } // GetCLADocuments fetches the cla documents inside of the CLA Group, it's separate method for perf reasons -func (r repository) GetCLADocuments(claGroupID string, claType string) ([]models.ClaGroupDocument, error) { +func (r Repository) GetCLADocuments(claGroupID string, claType string) ([]models.ClaGroupDocument, error) { log.Debugf("GetCLADocuments - claGroupID: %s - claType : %s", claGroupID, claType) dbModel, err := r.fetchCLAGroup(claGroupID) if err != nil { @@ -172,7 +201,7 @@ func (r repository) GetCLADocuments(claGroupID string, claType string) ([]models return projectDocuments, nil } -func (r repository) buildProjectDocuments(dbProjectDocumentModels []DBProjectDocumentModel) []models.ClaGroupDocument { +func (r Repository) buildProjectDocuments(dbProjectDocumentModels []DBProjectDocumentModel) []models.ClaGroupDocument { if len(dbProjectDocumentModels) == 0 { return nil } @@ -197,7 +226,7 @@ func (r repository) buildProjectDocuments(dbProjectDocumentModels []DBProjectDoc } // fetchCLAGroup brings back the CLA db model from dynamodb -func (r repository) fetchCLAGroup(claGroupID string) (*DBProjectModel, error) { +func (r Repository) fetchCLAGroup(claGroupID string) (*DBProjectModel, error) { var dbModel DBProjectModel tableName := fmt.Sprintf("cla-%s-projects", r.stage) @@ -223,7 +252,7 @@ func (r repository) fetchCLAGroup(claGroupID string) (*DBProjectModel, error) { } // buildProjectModel maps the database model to the API response model -func (r repository) buildProjectModel(dbModel DBProjectModel) *models.ClaGroup { +func (r Repository) buildProjectModel(dbModel DBProjectModel) *models.ClaGroup { return &models.ClaGroup{ ProjectID: dbModel.ProjectID, ProjectExternalID: dbModel.ProjectExternalID, @@ -239,7 +268,7 @@ func (r repository) buildProjectModel(dbModel DBProjectModel) *models.ClaGroup { } // UpdateDynamoContractGroupTemplates updates the templates in the data store -func (r repository) UpdateDynamoContractGroupTemplates(ctx context.Context, claGroupID string, template models.Template, pdfUrls models.TemplatePdfs, projectCCLAEnabled, projectICLAEnabled bool) error { +func (r Repository) UpdateDynamoContractGroupTemplates(ctx context.Context, claGroupID string, template models.Template, pdfUrls models.TemplatePdfs, projectCCLAEnabled, projectICLAEnabled bool) error { f := logrus.Fields{ "functionName": "UpdateDynamoContractGroupTemplates", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), @@ -495,7 +524,7 @@ var templateMap = map[string]models.Template{ ID: "mailing_address3", Name: "Mailing Address", AnchorString: "Mailing Address:", - FieldType: "text_unlocked", + FieldType: "text_optional", IsOptional: true, IsEditable: false, Width: 340, @@ -617,7 +646,7 @@ var templateMap = map[string]models.Template{ ID: "corporation_name", Name: "Corporation Name", AnchorString: "Corporation Name:", - FieldType: "text", + FieldType: "text_unlocked", IsOptional: false, IsEditable: false, Width: 355, @@ -653,7 +682,7 @@ var templateMap = map[string]models.Template{ ID: "corporation_address3", Name: "Corporation Address3", AnchorString: "Corporation Address:", - FieldType: "text_unlocked", + FieldType: "text_optional", IsOptional: true, IsEditable: true, Width: 350, @@ -820,7 +849,7 @@ var templateMap = map[string]models.Template{ ID: "mailing_address3", Name: "Mailing Address", AnchorString: "Mailing Address:", - FieldType: "text_unlocked", + FieldType: "text_optional", IsOptional: true, IsEditable: false, Width: 340, @@ -942,7 +971,7 @@ var templateMap = map[string]models.Template{ ID: "corporation_name", Name: "Corporation Name", AnchorString: "Corporation Name:", - FieldType: "text", + FieldType: "text_unlocked", IsOptional: false, IsEditable: false, Width: 355, @@ -978,7 +1007,7 @@ var templateMap = map[string]models.Template{ ID: "corporation_address3", Name: "Corporation Address3", AnchorString: "Corporation Address:", - FieldType: "text_unlocked", + FieldType: "text_optional", IsOptional: true, IsEditable: true, Width: 350, diff --git a/cla-backend-go/template/service.go b/cla-backend-go/template/service.go index 50e25cbaf..73a296eae 100644 --- a/cla-backend-go/template/service.go +++ b/cla-backend-go/template/service.go @@ -8,8 +8,9 @@ import ( "errors" "fmt" "io" - "io/ioutil" + "strconv" "strings" + "time" "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/sirupsen/logrus" @@ -18,7 +19,7 @@ import ( log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/docraptor" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" @@ -31,35 +32,38 @@ const ( claTypeCCLA = "ccla" ) -// Service interface -type Service interface { +// ServiceInterface interface +type ServiceInterface interface { GetTemplates(ctx context.Context) ([]models.Template, error) + GetTemplateName(ctx context.Context, templateID string) (string, error) CreateCLAGroupTemplate(ctx context.Context, claGroupID string, claGroupFields *models.CreateClaGroupTemplate) (models.TemplatePdfs, error) CreateTemplatePreview(ctx context.Context, claGroupFields *models.CreateClaGroupTemplate, templateFor string) ([]byte, error) GetCLATemplatePreview(ctx context.Context, claGroupID, claType string, watermark bool) ([]byte, error) + CLAGroupTemplateExists(ctx context.Context, templateID string) bool } -type service struct { +// Service object/struct +type Service struct { stage string // The AWS stage (dev, staging, prod) - templateRepo Repository - docraptorClient docraptor.Client + templateRepo RepositoryInterface + docRaptorClient docraptor.Client s3Client *s3manager.Uploader } // NewService API call -func NewService(stage string, templateRepo Repository, docraptorClient docraptor.Client, awsSession *session.Session) service { - return service{ +func NewService(stage string, templateRepo RepositoryInterface, docRaptorClient docraptor.Client, awsSession *session.Session) Service { + return Service{ stage: stage, templateRepo: templateRepo, - docraptorClient: docraptorClient, + docRaptorClient: docRaptorClient, s3Client: s3manager.NewUploader(awsSession), } } // GetTemplates API call -func (s service) GetTemplates(ctx context.Context) ([]models.Template, error) { +func (s Service) GetTemplates(ctx context.Context) ([]models.Template, error) { f := logrus.Fields{ - "functionName": "GetTemplates", + "functionName": "v1.template.service.GetTemplates", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), } log.WithFields(f).Debug("Loading templates...") @@ -79,9 +83,26 @@ func (s service) GetTemplates(ctx context.Context) ([]models.Template, error) { return templates, nil } -func (s service) CreateTemplatePreview(ctx context.Context, claGroupFields *models.CreateClaGroupTemplate, templateFor string) ([]byte, error) { +// GetTemplateName returns the template name when provided the template ID +func (s Service) GetTemplateName(ctx context.Context, templateID string) (string, error) { f := logrus.Fields{ - "functionName": "CreateTemplatePreview", + "functionName": "v1.template.service.GetTemplateName", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "templateID": templateID, + } + templateName, err := s.templateRepo.GetTemplateName(ctx, templateID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem loading template by ID: %s", templateID) + return "", err + } + + return templateName, nil +} + +// CreateTemplatePreview returns a PDF using the specified CLA Group field values and template identifier +func (s Service) CreateTemplatePreview(ctx context.Context, claGroupFields *models.CreateClaGroupTemplate, templateFor string) ([]byte, error) { + f := logrus.Fields{ + "functionName": "v1.template.service.CreateTemplatePreview", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "templateID": claGroupFields.TemplateID, "templateFor": templateFor, @@ -93,6 +114,7 @@ func (s service) CreateTemplatePreview(ctx context.Context, claGroupFields *mode if claGroupFields.TemplateID != "" { templateID = claGroupFields.TemplateID } + log.WithFields(f).Debugf("using template ID: %s", templateID) // Get Template template, err = s.templateRepo.GetTemplate(templateID) @@ -101,6 +123,7 @@ func (s service) CreateTemplatePreview(ctx context.Context, claGroupFields *mode claGroupFields.TemplateID) return nil, err } + log.WithFields(f).Debugf("loaded template ID: %s with ID: %s", template.Name, template.ID) // Apply template fields iclaTemplateHTML, cclaTemplateHTML, err := s.InjectProjectInformationIntoTemplate(template, claGroupFields.MetaFields) @@ -118,23 +141,31 @@ func (s service) CreateTemplatePreview(ctx context.Context, claGroupFields *mode return nil, errors.New("invalid value of template_for") } - pdf, err := s.docraptorClient.CreatePDF(templateHTML, templateFor) + ioReader, err := s.docRaptorClient.CreatePDF(templateHTML, templateFor) if err != nil { + log.WithFields(f).WithError(err).Warn("problem with API call to docraptor service") return nil, err } defer func() { - closeErr := pdf.Close() + closeErr := ioReader.Close() if closeErr != nil { log.WithFields(f).WithError(closeErr).Warn("error closing PDF") } }() - return ioutil.ReadAll(pdf) + + bytes, err := io.ReadAll(ioReader) + if err != nil { + log.WithFields(f).WithError(err).Warn("error reading PDF bytes from the generated template") + return nil, err + } + + return bytes, err } -// CreateCLAGroupTemplate -func (s service) CreateCLAGroupTemplate(ctx context.Context, claGroupID string, claGroupFields *models.CreateClaGroupTemplate) (models.TemplatePdfs, error) { +// CreateCLAGroupTemplate service method +func (s Service) CreateCLAGroupTemplate(ctx context.Context, claGroupID string, claGroupFields *models.CreateClaGroupTemplate) (models.TemplatePdfs, error) { f := logrus.Fields{ - "functionName": "CreateCLAGroupTemplate", + "functionName": "v1.template.service.CreateCLAGroupTemplate", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": claGroupID, "claGroupFields": claGroupFields, @@ -147,12 +178,10 @@ func (s service) CreateCLAGroupTemplate(ctx context.Context, claGroupID string, return models.TemplatePdfs{}, err } - // Verify the caller is authorized for the project that owns this CLA Group - // Get Template template, err := s.templateRepo.GetTemplate(claGroupFields.TemplateID) if err != nil { - log.WithFields(f).WithError(err).Warnf("Unable to fetch template fields: %s - returning empty template PDFs", + log.WithFields(f).WithError(err).Warnf("Unable to fetch template id: %s - returning empty template PDFs", claGroupFields.TemplateID) return models.TemplatePdfs{}, err } @@ -179,19 +208,19 @@ func (s service) CreateCLAGroupTemplate(ctx context.Context, claGroupID string, // Invoke the go routine - any errors will be handled below eg.Go(func() error { log.WithFields(f).Debugf("Creating PDF for %s", claTypeICLA) - iclaPdf, iclaErr := s.docraptorClient.CreatePDF(iclaTemplateHTML, claTypeICLA) + ioReader, iclaErr := s.docRaptorClient.CreatePDF(iclaTemplateHTML, claTypeICLA) if iclaErr != nil { log.WithFields(f).WithError(iclaErr).Warn("Problem generating ICLA template via docraptor client - returning empty template PDFs") return err } defer func() { - closeErr := iclaPdf.Close() + closeErr := ioReader.Close() if closeErr != nil { log.WithFields(f).WithError(closeErr).Warn("error closing ICLA PDF") } }() iclaFileName := s.generateTemplateS3FilePath(claGroupID, claTypeICLA) - iclaFileURL, err = s.SaveTemplateToS3(bucket, iclaFileName, iclaPdf) + iclaFileURL, err = s.SaveTemplateToS3(bucket, iclaFileName, ioReader) if err != nil { log.WithFields(f).WithError(err).Warnf("Problem uploading ICLA PDF: %s to s3 - returning empty template PDFs", iclaFileName) return err @@ -206,19 +235,19 @@ func (s service) CreateCLAGroupTemplate(ctx context.Context, claGroupID string, // Invoke the go routine - any errors will be handled below eg.Go(func() error { log.WithFields(f).Debugf("Creating PDF for %s", claTypeCCLA) - cclaPdf, cclaErr := s.docraptorClient.CreatePDF(cclaTemplateHTML, claTypeCCLA) + ioReader, cclaErr := s.docRaptorClient.CreatePDF(cclaTemplateHTML, claTypeCCLA) if cclaErr != nil { log.WithFields(f).WithError(cclaErr).Warn("Problem generating CCLA template via docraptor client - returning empty template PDFs") return err } defer func() { - closeErr := cclaPdf.Close() + closeErr := ioReader.Close() if closeErr != nil { log.WithFields(f).WithError(closeErr).Warn("error closing CCLA PDF") } }() cclaFileName := s.generateTemplateS3FilePath(claGroupID, claTypeCCLA) - cclaFileURL, err = s.SaveTemplateToS3(bucket, cclaFileName, cclaPdf) + cclaFileURL, err = s.SaveTemplateToS3(bucket, cclaFileName, ioReader) if err != nil { log.WithFields(f).Warnf("Problem uploading CCLA PDF: %s to s3, error: %v - returning empty template PDFs", cclaFileName, err) return err @@ -263,9 +292,10 @@ func (s service) CreateCLAGroupTemplate(ctx context.Context, claGroupID string, return pdfUrls, nil } -func (s service) GetCLATemplatePreview(ctx context.Context, claGroupID, claType string, watermark bool) ([]byte, error) { +// GetCLATemplatePreview returns a preview of the specified CLA Group and CLA type +func (s Service) GetCLATemplatePreview(ctx context.Context, claGroupID, claType string, watermark bool) ([]byte, error) { f := logrus.Fields{ - "functionName": "GetCLATemplatePreview", + "functionName": "v1.template.service.GetCLATemplatePreview", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": claGroupID, "claType": claType, @@ -315,7 +345,7 @@ func (s service) GetCLATemplatePreview(ctx context.Context, claGroupID, claType return nil, err } - doc := claGroupDocuments[0] + doc := getLatestDocument(ctx, claGroupDocuments) pdfS3URL := doc.DocumentS3URL if pdfS3URL == "" { err = fmt.Errorf("s3 url is empty for groupID : %s and document %s", claGroupID, doc.DocumentFileID) @@ -355,8 +385,88 @@ func (s service) GetCLATemplatePreview(ctx context.Context, claGroupID, claType return b, nil } -// InjectProjectInformationIntoTemplate -func (s service) InjectProjectInformationIntoTemplate(template models.Template, metaFields []*models.MetaField) (string, string, error) { +func getLatestDocument(ctx context.Context, documents []models.ClaGroupDocument) *models.ClaGroupDocument { + f := logrus.Fields{ + "functionName": "v1.template.service.getLatestDocument", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + var latestDocument *models.ClaGroupDocument + var latestMajorVersion = 0 + var latestMinorVersion = 0 + var latestDateTime time.Time + for _, currentDocument := range documents { + if latestDocument == nil { + latestDocument = ¤tDocument // nolint + // Grab and save the major version + major, convertErr := strconv.Atoi(latestDocument.DocumentMajorVersion) + if convertErr != nil { + log.WithFields(f).WithError(convertErr).Warnf("problem converting document major version to int: %s", latestDocument.DocumentMajorVersion) + major = 0 + } + latestMajorVersion = major + + // Grab and save the major version + minor, convertErr := strconv.Atoi(latestDocument.DocumentMinorVersion) + if convertErr != nil { + log.WithFields(f).WithError(convertErr).Warnf("problem converting document minor version to int: %s", latestDocument.DocumentMinorVersion) + minor = 0 + } + latestMinorVersion = minor + + dateTime, dateTimeErr := utils.ParseDateTime(latestDocument.DocumentCreationDate) + if dateTimeErr != nil { + log.WithFields(f).WithError(dateTimeErr).Warnf("problem converting document creation date to time object: %s", latestDocument.DocumentCreationDate) + } + latestDateTime = dateTime + + continue + } + + // Grab and save the major version + major, convertErr := strconv.Atoi(currentDocument.DocumentMajorVersion) + if convertErr != nil { + log.WithFields(f).WithError(convertErr).Warnf("problem converting document major version to int: %s", currentDocument.DocumentMajorVersion) + major = 0 + } + + // Grab and save the major version + minor, convertErr := strconv.Atoi(currentDocument.DocumentMinorVersion) + if convertErr != nil { + log.WithFields(f).WithError(convertErr).Warnf("problem converting document minor version to int: %s", currentDocument.DocumentMinorVersion) + minor = 0 + } + + dateTime, dateTimeErr := utils.ParseDateTime(currentDocument.DocumentCreationDate) + if dateTimeErr != nil { + log.WithFields(f).WithError(dateTimeErr).Warnf("problem converting document creation date to time object: %s", currentDocument.DocumentCreationDate) + } + + if major > latestMajorVersion { + latestDocument = ¤tDocument // nolint + continue + } + + if minor > latestMinorVersion { + latestDocument = ¤tDocument // nolint + continue + } + + if dateTime.After(latestDateTime) { + latestDocument = ¤tDocument // nolint + continue + } + } + + return latestDocument +} + +// InjectProjectInformationIntoTemplate service function +func (s Service) InjectProjectInformationIntoTemplate(template models.Template, metaFields []*models.MetaField) (string, string, error) { + f := logrus.Fields{ + "functionName": "v1.template.service.InjectProjectInformationIntoTemplate", + "templateName": template.Name, + "templateID": template.ID, + } lookupMap := map[string]models.MetaField{} for _, field := range template.MetaFields { lookupMap[field.Name] = *field @@ -381,11 +491,13 @@ func (s service) InjectProjectInformationIntoTemplate(template models.Template, return "", "", errors.New("bad request: required fields for template were not found") } + log.WithFields(f).Debugf("Rendering ICLA body for template: %s with id: %s", template.Name, template.ID) iclaTemplateHTML, err := raymond.Render(template.IclaHTMLBody, metaFieldsMap) if err != nil { return "", "", err } + log.WithFields(f).Debugf("Rendering CCLA body for template: %s with id: %s", template.Name, template.ID) cclaTemplateHTML, err := raymond.Render(template.CclaHTMLBody, metaFieldsMap) if err != nil { return "", "", err @@ -395,7 +507,7 @@ func (s service) InjectProjectInformationIntoTemplate(template models.Template, } // generateTemplateS3FilePath helper function to generate a suitable s3 path and filename for the template -func (s service) generateTemplateS3FilePath(claGroupID, claType string) string { +func (s Service) generateTemplateS3FilePath(claGroupID, claType string) string { fileNameTemplate := "contract-group/%s/template/%s" var ext string switch claType { @@ -411,10 +523,10 @@ func (s service) generateTemplateS3FilePath(claGroupID, claType string) string { return fileName } -// SaveTemplateToS3 -func (s service) SaveTemplateToS3(bucket, filepath string, template io.ReadCloser) (string, error) { +// SaveTemplateToS3 uploads the specified template contents to S3 storage +func (s Service) SaveTemplateToS3(bucket, filepath string, template io.ReadCloser) (string, error) { f := logrus.Fields{ - "functionName": "SaveTemplateToS3", + "functionName": "v1.template.service.SaveTemplateToS3", "bucket": bucket, "filepath": filepath, } @@ -439,3 +551,8 @@ func (s service) SaveTemplateToS3(bucket, filepath string, template io.ReadClose return result.Location, nil } + +// CLAGroupTemplateExists return true if the specified template ID exists, false otherwise +func (s Service) CLAGroupTemplateExists(ctx context.Context, templateID string) bool { + return s.templateRepo.CLAGroupTemplateExists(ctx, templateID) +} diff --git a/cla-backend-go/tests/events_test.go b/cla-backend-go/tests/events_test.go index ba8e3a74b..02d520a56 100644 --- a/cla-backend-go/tests/events_test.go +++ b/cla-backend-go/tests/events_test.go @@ -8,7 +8,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/communitybridge/easycla/cla-backend-go/events" - eventOps "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/events" + eventOps "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/events" "github.com/stretchr/testify/assert" ) @@ -32,5 +32,5 @@ func TestEventsService(t *testing.T) { CompanyID: aws.String("company-1234"), }) assert.Nil(t, err, "Error is nil") - assert.Equal(t, len(eventsSearch.Events), 1) + assert.Equal(t, 1, len(eventsSearch.Events)) } diff --git a/cla-backend-go/tests/github_user_test.go b/cla-backend-go/tests/github_user_test.go new file mode 100644 index 000000000..d83266036 --- /dev/null +++ b/cla-backend-go/tests/github_user_test.go @@ -0,0 +1,58 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package tests + +import ( + "fmt" + "os" + "testing" + + "github.com/communitybridge/easycla/cla-backend-go/github" + ini "github.com/communitybridge/easycla/cla-backend-go/init" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestGitHubGetUserDetails(t *testing.T) { + if gitHubTestsEnabled { + // Need to initialize the system to load the configuration which contains a number of SSM parameters + stage := os.Getenv("STAGE") + if stage == "" { + assert.Fail(t, "set STAGE environment variable to run unit and functional tests.") + } + dynamodbRegion := os.Getenv("DYNAMODB_AWS_REGION") + if dynamodbRegion == "" { + assert.Fail(t, "set DYNAMODB_AWS_REGION environment variable to run unit and functional tests.") + } + + viper.Set("STAGE", stage) + viper.Set("DYNAMODB_AWS_REGION", dynamodbRegion) + ini.Init() + _, err := ini.GetAWSSession() + if err != nil { + assert.Fail(t, "unable to load AWS session", err) + } + ini.ConfigVariable() + config := ini.GetConfig() + github.Init(config.GitHub.AppID, config.GitHub.AppPrivateKey, config.GitHub.AccessToken) + + // Test data - dogfood my own user account + var gitHubUsername = "dealako" + var gitHubUserEmail = "ddeal@linuxfoundation.org" + var gitHubUserID = int64(519609) + + gitHubUserModel, gitHubErr := github.GetUserDetails(gitHubUsername) + assert.Nil(t, gitHubErr, fmt.Sprintf("unable to get GitHub user details using GitHub username: %s", gitHubUsername)) + assert.NotNil(t, gitHubUserModel, fmt.Sprintf("GitHub user model is nil for GitHub username: %s", gitHubUsername)) + + assert.NotNil(t, gitHubUserModel.Login, fmt.Sprintf("GitHub user model login value is nil for GitHub username: %s", gitHubUsername)) + assert.Equal(t, gitHubUsername, *gitHubUserModel.Login, fmt.Sprintf("GitHub username does not match for GitHub username: %s", gitHubUsername)) + + assert.NotNil(t, gitHubUserModel.ID, fmt.Sprintf("GitHub user model response ID is nil for GitHub username: %s", gitHubUsername)) + assert.Equal(t, gitHubUserID, *gitHubUserModel.ID, fmt.Sprintf("GitHub user ID does not match for GitHub username: %s - expecting: %d", gitHubUsername, gitHubUserID)) + + assert.NotNil(t, gitHubUserModel.Email, fmt.Sprintf("GitHub user model email is nil for GitHub username: %s", gitHubUsername)) + assert.Equal(t, gitHubUserEmail, *gitHubUserModel.Email, fmt.Sprintf("GitHub user email does not match for GitHub username: %s - expecting: %s", gitHubUsername, gitHubUserEmail)) + } +} diff --git a/cla-backend-go/tests/github_v4_test.go b/cla-backend-go/tests/github_v4_test.go new file mode 100644 index 000000000..3a2441545 --- /dev/null +++ b/cla-backend-go/tests/github_v4_test.go @@ -0,0 +1,65 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package tests + +import ( + "fmt" + "os" + "strconv" + "testing" + + "github.com/communitybridge/easycla/cla-backend-go/github/branch_protection" + + ini "github.com/communitybridge/easycla/cla-backend-go/init" + "github.com/spf13/viper" + + "github.com/communitybridge/easycla/cla-backend-go/github" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/stretchr/testify/assert" +) + +const gitHubTestsEnabled = false // nolint + +func TestGetRepositoryIDFromName(t *testing.T) { + if gitHubTestsEnabled { + ctx := utils.NewContext() + + // Need to initialize the system to load the configuration which contains a number of SSM parameters + stage := os.Getenv("STAGE") + if stage == "" { + assert.Fail(t, "set STAGE environment variable to run unit and functional tests.") + } + dynamodbRegion := os.Getenv("DYNAMODB_AWS_REGION") + if dynamodbRegion == "" { + assert.Fail(t, "set DYNAMODB_AWS_REGION environment variable to run unit and functional tests.") + } + + viper.Set("STAGE", stage) + viper.Set("DYNAMODB_AWS_REGION", dynamodbRegion) + ini.Init() + _, err := ini.GetAWSSession() + if err != nil { + assert.Fail(t, "unable to load AWS session", err) + } + ini.ConfigVariable() + config := ini.GetConfig() + github.Init(config.GitHub.AppID, config.GitHub.AppPrivateKey, config.GitHub.AccessToken) + installationID, int64Err := strconv.ParseInt(config.GitHub.TestOrganizationInstallationID, 10, 64) + if int64Err != nil { + assert.Fail(t, fmt.Sprintf("unable to convert installation ID to string: %s", config.GitHub.TestOrganizationInstallationID), int64Err) + } + + branchProtectionRepoV4, err := branch_protection.NewBranchProtectionRepositoryV4(installationID) + if err != nil { + assert.Fail(t, fmt.Sprintf("initializing branch protection v4 repo failed : %v", err)) + } + expectedValue := config.GitHub.TestRepositoryID + actualValue, err := branchProtectionRepoV4.GetRepositoryIDFromName(ctx, config.GitHub.TestOrganization, config.GitHub.TestRepository) + if err != nil { + assert.Fail(t, fmt.Sprintf("unable to create GitHub v4 client from installation ID: %d", installationID), err) + } + assert.Equal(t, expectedValue, actualValue, "CombinedRepository ID Lookup") + } + +} diff --git a/cla-backend-go/tests/gitlab_access_token_decode_test.go b/cla-backend-go/tests/gitlab_access_token_decode_test.go new file mode 100644 index 000000000..8ad05bf0d --- /dev/null +++ b/cla-backend-go/tests/gitlab_access_token_decode_test.go @@ -0,0 +1,54 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package tests + +import ( + "os" + "testing" + + gitlab_api "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" + ini "github.com/communitybridge/easycla/cla-backend-go/init" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/communitybridge/easycla/cla-backend-go/v2/gitlab_organizations" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestGitLabAccessTokenDecode(t *testing.T) { // no lint + if false { // nolint + ctx := utils.NewContext() + // Need to initialize the system to load the configuration which contains a number of SSM parameters + stage := os.Getenv("STAGE") + if stage == "" { + assert.Fail(t, "set STAGE environment variable to run unit and functional tests.") + } + dynamodbRegion := os.Getenv("DYNAMODB_AWS_REGION") + if dynamodbRegion == "" { + assert.Fail(t, "set DYNAMODB_AWS_REGION environment variable to run unit and functional tests.") + } + + viper.Set("STAGE", stage) + viper.Set("DYNAMODB_AWS_REGION", dynamodbRegion) + ini.Init() + awsSession, err := ini.GetAWSSession() + if err != nil { + assert.Fail(t, "unable to load AWS session", err) + } + ini.ConfigVariable() + config := ini.GetConfig() + + // Create a new GitLab App client instance + gitLabApp := gitlab_api.Init(config.Gitlab.AppClientID, config.Gitlab.AppClientSecret, config.Gitlab.AppPrivateKey) + + gitLabOrgRepo := gitlab_organizations.NewRepository(awsSession, stage) + + gitlabOrg, err := gitLabOrgRepo.GetGitLabOrganizationByFullPath(ctx, "linuxfoundation/product/easycla") + assert.Nil(t, err, "get gitlab organization by name error should be nil") + assert.NotNil(t, gitlabOrg, "gitlab organization should not nil") + oauthResp, err := gitlab_api.DecryptAuthInfo(gitlabOrg.AuthInfo, gitLabApp) + assert.Nil(t, err, "decrypt auth info error should be nil") + assert.NotNil(t, oauthResp, "oauth response should not be nil") + t.Logf("decoded oauth client with access token : %s", oauthResp.AccessToken) + } +} diff --git a/cla-backend-go/tests/gitlab_client_test.go b/cla-backend-go/tests/gitlab_client_test.go new file mode 100644 index 000000000..3f00dcd46 --- /dev/null +++ b/cla-backend-go/tests/gitlab_client_test.go @@ -0,0 +1,379 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package tests + +import ( + "fmt" + "io" + "os" + "testing" + + ini "github.com/communitybridge/easycla/cla-backend-go/init" + "github.com/spf13/viper" + + "github.com/communitybridge/easycla/cla-backend-go/utils" + + gitlab_api "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" + "github.com/stretchr/testify/assert" + "github.com/xanzy/go-gitlab" +) + +const gitLabTestsEnabled = false // nolint +const group = "The Linux Foundation/product/EasyCLA" // nolint +const accessInfo = "" + +const easyCLAGroupName = "linuxfoundation/product/easycla" // nolint + +func TestGetGroupByName(t *testing.T) { // no lint + if gitLabTestsEnabled { // nolint + // Need to initialize the system to load the configuration which contains a number of SSM parameters + stage := os.Getenv("STAGE") + if stage == "" { + assert.Fail(t, "set STAGE environment variable to run unit and functional tests.") + } + dynamodbRegion := os.Getenv("DYNAMODB_AWS_REGION") + if dynamodbRegion == "" { + assert.Fail(t, "set DYNAMODB_AWS_REGION environment variable to run unit and functional tests.") + } + + viper.Set("STAGE", stage) + viper.Set("DYNAMODB_AWS_REGION", dynamodbRegion) + ini.Init() + _, err := ini.GetAWSSession() + if err != nil { + assert.Fail(t, "unable to load AWS session", err) + } + ini.ConfigVariable() + config := ini.GetConfig() + + // Create a new GitLab App client instance + gitLabApp := gitlab_api.Init(config.Gitlab.AppClientID, config.Gitlab.AppClientSecret, config.Gitlab.AppPrivateKey) + + // Create a new client + gitLabClient, err := gitlab_api.NewGitlabOauthClient(accessInfo, gitLabApp) + assert.Nil(t, err, "GitLab OAuth Client Error is Nil") + assert.NotNil(t, gitLabClient, "GitLab OAuth Client is Not Nil") + + ctx := utils.NewContext() + // Need to look up the GitLab Group/Organization to obtain the ID + //groupModel, getError := gitlab_api.GetGroupByName(ctx, gitLabClient, easyCLAGroupName) + //groupModel, getError := gitlab_api.GetGroupByName(ctx, gitLabClient, "EasyCLA") + groupModel, getError := gitlab_api.GetGroupByName(ctx, gitLabClient, "linuxfoundation/product/asitha") + assert.Nil(t, getError, "GitLab GetGroup Error should be nil", getError) + assert.NotNil(t, groupModel, "Group Model should not be nil") + t.Logf("group ID: %d, name: %s, path: %s, full path: %s", groupModel.ID, groupModel.Name, groupModel.Path, groupModel.FullPath) + } +} + +func TestGetGroupByID(t *testing.T) { // no lint + if gitLabTestsEnabled { // nolint + // Need to initialize the system to load the configuration which contains a number of SSM parameters + stage := os.Getenv("STAGE") + if stage == "" { + assert.Fail(t, "set STAGE environment variable to run unit and functional tests.") + } + dynamodbRegion := os.Getenv("DYNAMODB_AWS_REGION") + if dynamodbRegion == "" { + assert.Fail(t, "set DYNAMODB_AWS_REGION environment variable to run unit and functional tests.") + } + + viper.Set("STAGE", stage) + viper.Set("DYNAMODB_AWS_REGION", dynamodbRegion) + ini.Init() + _, err := ini.GetAWSSession() + if err != nil { + assert.Fail(t, "unable to load AWS session", err) + } + ini.ConfigVariable() + config := ini.GetConfig() + + // Create a new GitLab App client instance + gitLabApp := gitlab_api.Init(config.Gitlab.AppClientID, config.Gitlab.AppClientSecret, config.Gitlab.AppPrivateKey) + + // Create a new client + gitLabClient, err := gitlab_api.NewGitlabOauthClient(accessInfo, gitLabApp) + assert.Nil(t, err, "GitLab OAuth Client Error is Nil") + assert.NotNil(t, gitLabClient, "GitLab OAuth Client is Not Nil") + + ctx := utils.NewContext() + groupModel, getError := gitlab_api.GetGroupByID(ctx, gitLabClient, 13050017) + assert.Nil(t, getError, "GitLab GetGroup Error should be nil", getError) + assert.NotNil(t, groupModel, "Group Model should not be nil") + t.Logf("group ID: %d, name: %s, path: %s, full path: %s", groupModel.ID, groupModel.Name, groupModel.Path, groupModel.FullPath) + } +} + +func TestGetGroupByFullPath(t *testing.T) { // no lint + if gitLabTestsEnabled { // nolint + // Need to initialize the system to load the configuration which contains a number of SSM parameters + stage := os.Getenv("STAGE") + if stage == "" { + assert.Fail(t, "set STAGE environment variable to run unit and functional tests.") + } + dynamodbRegion := os.Getenv("DYNAMODB_AWS_REGION") + if dynamodbRegion == "" { + assert.Fail(t, "set DYNAMODB_AWS_REGION environment variable to run unit and functional tests.") + } + + viper.Set("STAGE", stage) + viper.Set("DYNAMODB_AWS_REGION", dynamodbRegion) + ini.Init() + _, err := ini.GetAWSSession() + if err != nil { + assert.Fail(t, "unable to load AWS session", err) + } + ini.ConfigVariable() + config := ini.GetConfig() + + // Create a new GitLab App client instance + gitLabApp := gitlab_api.Init(config.Gitlab.AppClientID, config.Gitlab.AppClientSecret, config.Gitlab.AppPrivateKey) + + // Create a new client + gitLabClient, err := gitlab_api.NewGitlabOauthClient(accessInfo, gitLabApp) + assert.Nil(t, err, "GitLab OAuth Client Error is Nil") + assert.NotNil(t, gitLabClient, "GitLab OAuth Client is Not Nil") + + ctx := utils.NewContext() + groupModel, getError := gitlab_api.GetGroupByFullPath(ctx, gitLabClient, "linuxfoundation/product/asitha") + assert.Nil(t, getError, "GitLab GetGroup Error should be nil", getError) + assert.NotNil(t, groupModel, "Group Model should not be nil") + t.Logf("group ID: %d, name: %s, path: %s, full path: %s", groupModel.ID, groupModel.Name, groupModel.Path, groupModel.FullPath) + } +} + +func TestGetGroupProjectListByGroupID(t *testing.T) { // no lint + if gitLabTestsEnabled { // nolint + // Need to initialize the system to load the configuration which contains a number of SSM parameters + stage := os.Getenv("STAGE") + if stage == "" { + assert.Fail(t, "set STAGE environment variable to run unit and functional tests.") + } + dynamodbRegion := os.Getenv("DYNAMODB_AWS_REGION") + if dynamodbRegion == "" { + assert.Fail(t, "set DYNAMODB_AWS_REGION environment variable to run unit and functional tests.") + } + + viper.Set("STAGE", stage) + viper.Set("DYNAMODB_AWS_REGION", dynamodbRegion) + ini.Init() + _, err := ini.GetAWSSession() + if err != nil { + assert.Fail(t, "unable to load AWS session", err) + } + ini.ConfigVariable() + config := ini.GetConfig() + + // Create a new GitLab App client instance + gitLabApp := gitlab_api.Init(config.Gitlab.AppClientID, config.Gitlab.AppClientSecret, config.Gitlab.AppPrivateKey) + + // Create a new client + gitLabClient, err := gitlab_api.NewGitlabOauthClient(accessInfo, gitLabApp) + assert.Nil(t, err, "GitLab OAuth Client Error is Nil") + assert.NotNil(t, gitLabClient, "GitLab OAuth Client is Not Nil") + + ctx := utils.NewContext() + gitLabProjects, getError := gitlab_api.GetGroupProjectListByGroupID(ctx, gitLabClient, 13050017) + assert.Nil(t, getError, "Get Group Projects List by Group ID error should be nil", getError) + assert.NotNil(t, gitLabProjects, "Get Group Projects Array should not be nil") + assert.Greaterf(t, len(gitLabProjects), 0, "Get Group Projects Array greater than zero: %d", len(gitLabProjects)) + for _, p := range gitLabProjects { + t.Logf("id: %d, name: %s, web url: %s, path: %s, full path: %s", p.ID, p.Name, p.WebURL, p.Path, p.PathWithNamespace) + } + } +} + +func TestGitLabListGroups(t *testing.T) { // no lint + + if gitLabTestsEnabled { // nolint + // Need to initialize the system to load the configuration which contains a number of SSM parameters + stage := os.Getenv("STAGE") + if stage == "" { + assert.Fail(t, "set STAGE environment variable to run unit and functional tests.") + } + dynamodbRegion := os.Getenv("DYNAMODB_AWS_REGION") + if dynamodbRegion == "" { + assert.Fail(t, "set DYNAMODB_AWS_REGION environment variable to run unit and functional tests.") + } + + viper.Set("STAGE", stage) + viper.Set("DYNAMODB_AWS_REGION", dynamodbRegion) + ini.Init() + _, err := ini.GetAWSSession() + if err != nil { + assert.Fail(t, "unable to load AWS session", err) + } + ini.ConfigVariable() + config := ini.GetConfig() + + // Create a new GitLab App client instance + gitLabApp := gitlab_api.Init(config.Gitlab.AppClientID, config.Gitlab.AppClientSecret, config.Gitlab.AppPrivateKey) + + // Create a new client + gitLabClient, err := gitlab_api.NewGitlabOauthClient(accessInfo, gitLabApp) + assert.Nil(t, err, "GitLab OAuth Client Error is Nil") + assert.NotNil(t, gitLabClient, "GitLab OAuth Client is Not Nil") + + // Need to look up the GitLab Group/Organization to obtain the ID + opts := &gitlab.ListGroupsOptions{ + ListOptions: gitlab.ListOptions{ + Page: 1, + PerPage: 100, + }, + } + + groups, resp, searchErr := gitLabClient.Groups.ListGroups(opts) + assert.Nil(t, searchErr, "GitLab List Groups Error is Nil") + if searchErr != nil { + t.Logf("list groups error: %+v", searchErr) + } + if resp.StatusCode < 200 || resp.StatusCode > 299 { + respBody, readErr := io.ReadAll(resp.Body) + assert.Nil(t, readErr, "GitLab Response Body Read is Nil") + assert.Fail(t, fmt.Sprintf("unable to list GitLab groups, status code: %d, body: %s", resp.StatusCode, respBody)) + } + for _, g := range groups { + t.Logf("name: %s, id: %d, path: %s, full path: %s, web url: %s", g.Name, g.ID, g.Path, g.FullPath, g.WebURL) + } + } +} + +func TestGitLabListProjects(t *testing.T) { // no lint + + if gitLabTestsEnabled { // nolint + // Need to initialize the system to load the configuration which contains a number of SSM parameters + stage := os.Getenv("STAGE") + if stage == "" { + assert.Fail(t, "set STAGE environment variable to run unit and functional tests.") + } + dynamodbRegion := os.Getenv("DYNAMODB_AWS_REGION") + if dynamodbRegion == "" { + assert.Fail(t, "set DYNAMODB_AWS_REGION environment variable to run unit and functional tests.") + } + + viper.Set("STAGE", stage) + viper.Set("DYNAMODB_AWS_REGION", dynamodbRegion) + ini.Init() + _, err := ini.GetAWSSession() + if err != nil { + assert.Fail(t, "unable to load AWS session", err) + } + ini.ConfigVariable() + config := ini.GetConfig() + + // Create a new GitLab App client instance + gitLabApp := gitlab_api.Init(config.Gitlab.AppClientID, config.Gitlab.AppClientSecret, config.Gitlab.AppPrivateKey) + + // Create a new client + gitLabClient, err := gitlab_api.NewGitlabOauthClient(accessInfo, gitLabApp) + assert.Nil(t, err, "GitLab OAuth Client Error is Nil") + assert.NotNil(t, gitLabClient, "GitLab OAuth Client is Not Nil") + + // Query GitLab for repos - fetch the list of repositories available to the GitLab App + listProjectsOpts := &gitlab.ListProjectsOptions{ + ListOptions: gitlab.ListOptions{ + Page: 1, // starts with one: https://docs.gitlab.com/ee/api/#offset-based-pagination + PerPage: 100, + }, + Archived: nil, + Visibility: nil, + OrderBy: nil, + Sort: nil, + Search: utils.StringRef("linuxfoundation"), + SearchNamespaces: utils.Bool(true), + Simple: nil, + Owned: nil, + Membership: utils.Bool(true), + Starred: nil, + Statistics: nil, + Topic: nil, + WithCustomAttributes: nil, + WithIssuesEnabled: nil, + WithMergeRequestsEnabled: nil, + WithProgrammingLanguage: nil, + WikiChecksumFailed: nil, + RepositoryChecksumFailed: nil, + MinAccessLevel: gitlab.AccessLevel(gitlab.MaintainerPermissions), + IDAfter: nil, + IDBefore: nil, + LastActivityAfter: nil, + LastActivityBefore: nil, + } + + // Need to use this func to get the list of projects the user has access to, see: https://gitlab.com/gitlab-org/gitlab-foss/-/issues/63811 + projects, resp, listProjectsErr := gitLabClient.Projects.ListProjects(listProjectsOpts) + assert.Nil(t, listProjectsErr, "GitLab OAuth Client") + if resp.StatusCode < 200 || resp.StatusCode > 299 { + assert.Fail(t, "unable to locate GitLab group by name: %s, status code: %d", group, resp.StatusCode) + } + + // DEBUG + //t.Logf("Recevied %d projects", len(projects)) + //for _, p := range projects { + // t.Logf("project name: %s, ID: %d, path: %s", p.Name, p.ID, p.PathWithNamespace) + //} + + // DEBUG + //t.Log("projects:") + //for _, p := range projects { + //byteArr, err := json.Marshal(p) + //assert.Nil(t, err) + //t.Logf("project: %s", byteArr) + //} + + if len(projects) > 1 { + assert.Fail(t, fmt.Sprintf("expecting > 1 result for GitLab list projects, found: %d - %+v", len(projects), projects)) + } + } +} + +func TestGitLabGetUserByUsername(t *testing.T) { + + if gitLabTestsEnabled { // nolint + // Need to initialize the system to load the configuration which contains a number of SSM parameters + stage := os.Getenv("STAGE") + if stage == "" { + assert.Fail(t, "set STAGE environment variable to run unit and functional tests.") + } + dynamodbRegion := os.Getenv("DYNAMODB_AWS_REGION") + if dynamodbRegion == "" { + assert.Fail(t, "set DYNAMODB_AWS_REGION environment variable to run unit and functional tests.") + } + + viper.Set("STAGE", stage) + viper.Set("DYNAMODB_AWS_REGION", dynamodbRegion) + ini.Init() + _, err := ini.GetAWSSession() + if err != nil { + assert.Fail(t, "unable to load AWS session", err) + } + ini.ConfigVariable() + config := ini.GetConfig() + + // Create a new GitLab App client instance + gitLabApp := gitlab_api.Init(config.Gitlab.AppClientID, config.Gitlab.AppClientSecret, config.Gitlab.AppPrivateKey) + assert.NotNil(t, gitLabApp, "GitLab App reference is Not Nil") + + // Create a new client + gitLabClient, err := gitlab_api.NewGitlabOauthClient(accessInfo, gitLabApp) + assert.Nil(t, err, "GitLab OAuth Client Error is Nil") + assert.NotNil(t, gitLabClient, "GitLab OAuth Client is Not Nil") + + // Test data - dogfood my own user account + var gitLabUsername = "dealako" + + opts := &gitlab.ListUsersOptions{ + ListOptions: gitlab.ListOptions{ + Page: 1, // starts with one: https://docs.gitlab.com/ee/api/#offset-based-pagination + PerPage: 100, + }, + Username: utils.StringRef(gitLabUsername), + } + userList, resp, err := gitLabClient.Users.ListUsers(opts, nil) + assert.Nil(t, err, "GitLab OAuth Client") + if resp.StatusCode < 200 || resp.StatusCode > 299 { + assert.Failf(t, "GitLab List Users API Response Error", "unable to locate GitLab user by name: %s, status code: %d", gitLabUsername, resp.StatusCode) + } + assert.NotEqualf(t, 1, len(userList), "GitLab List Users Response Error - expecting 1 result for GitLab list users, found: %d - %+v", len(userList), userList) + } +} diff --git a/cla-backend-go/tests/project_helpers_test.go b/cla-backend-go/tests/project_helpers_test.go new file mode 100644 index 000000000..5f9aa3fe3 --- /dev/null +++ b/cla-backend-go/tests/project_helpers_test.go @@ -0,0 +1,166 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package tests + +import ( + "testing" + + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/communitybridge/easycla/cla-backend-go/v2/project-service/models" + "github.com/go-openapi/strfmt" + "github.com/stretchr/testify/assert" +) + +const ( + testProjectID = "def13456" + testProjectLogo = "testlogurl.com" +) + +func TestIsProjectHasRootParentNoParent(t *testing.T) { + project := &models.ProjectOutputDetailed{} + project.Foundation = nil + assert.True(t, utils.IsProjectHasRootParent(project), "Project Has Root Parent - Empty Parent") +} + +func TestIsProjectHasRootParentLF(t *testing.T) { + project := &models.ProjectOutputDetailed{} + project.Foundation = &models.Foundation{ + ID: testProjectID, + LogoURL: testProjectLogo, + Name: utils.TheLinuxFoundation, + } + assert.True(t, utils.IsProjectHasRootParent(project), "Project Has Root Parent - LF Parent") +} + +func TestIsProjectHasRootParentLFProjectsLLCFalse(t *testing.T) { + project := &models.ProjectOutputDetailed{} + project.Foundation = &models.Foundation{ + ID: testProjectID, + LogoURL: testProjectLogo, + Name: utils.LFProjectsLLC, + } + assert.False(t, utils.IsProjectHasRootParent(project), "Project Has Root Parent - LF Projects LLC Parent") +} + +func TestIsProjectHasRootParentNonLF(t *testing.T) { + project := &models.ProjectOutputDetailed{} + project.Foundation = &models.Foundation{ + ID: testProjectID, + LogoURL: testProjectLogo, + Name: "other", + } + assert.False(t, utils.IsProjectHasRootParent(project), "Project Should not have Root Parent as - LF Project Parent") +} + +func TestIsStandaloneProject(t *testing.T) { + project := &models.ProjectOutputDetailed{} + project.Foundation = nil + project.Projects = []*models.ProjectOutput{} + assert.True(t, utils.IsStandaloneProject(project), "Standalone Project with No Parent with No Children") +} + +func TestLFParent(t *testing.T) { + project := &models.ProjectOutputDetailed{} + project.Foundation = &models.Foundation{ + ID: testProjectID, + LogoURL: testProjectLogo, + Name: utils.TheLinuxFoundation, + } + project.Projects = []*models.ProjectOutput{} + assert.True(t, utils.IsStandaloneProject(project), "Standalone Project with LF Parent with No Children") +} + +func TestLFProjectsLLCParent(t *testing.T) { + project := &models.ProjectOutputDetailed{} + project.Foundation = &models.Foundation{ + ID: testProjectID, + LogoURL: testProjectLogo, + Name: utils.LFProjectsLLC, + } + project.Projects = []*models.ProjectOutput{} + assert.False(t, utils.IsStandaloneProject(project), "Should not be a standalone Project with LF Projects LLC parent with No Children") +} + +func TestLFParentWithChildren(t *testing.T) { + project := &models.ProjectOutputDetailed{} + project.Foundation = &models.Foundation{ + ID: testProjectID, + LogoURL: testProjectLogo, + Name: utils.TheLinuxFoundation, + } + project.Projects = []*models.ProjectOutput{} + assert.True(t, utils.IsStandaloneProject(project), "Standalone Project with LF Parent with Children") +} + +func TestLFProjectsLLCParentWithChildren(t *testing.T) { + project := &models.ProjectOutputDetailed{} + project.Foundation = &models.Foundation{ + ID: testProjectID, + LogoURL: testProjectLogo, + Name: utils.LFProjectsLLC, + } + project.Projects = []*models.ProjectOutput{} + child := &models.ProjectOutput{ + ProjectCommon: models.ProjectCommon{}, + CreatedDate: nil, + DocuSignStatus: nil, + EndDate: nil, + EntityType: "", + ExecutiveDirector: nil, + Foundation: nil, + HerokuConnectID: "", + ID: testProjectID, + IsDeleted: false, + LFSponsored: false, + LegalParent: nil, + ModifiedDate: nil, + OpportunityOwner: nil, + Owner: nil, + ProgramManager: nil, + ProjectType: "SubProject", + RenewalOwner: nil, + Slug: "another-slug", + SystemModStamp: strfmt.DateTime{}, + Type: "", + } + project.Projects = []*models.ProjectOutput{child} + assert.False(t, utils.IsStandaloneProject(project), "Standalone Project with LF Projects LLC parent with Children") +} + +func TestIsProjectHaveChildrenNoChildren(t *testing.T) { + project := &models.ProjectOutputDetailed{} + project.Foundation = nil + project.Projects = []*models.ProjectOutput{} + assert.False(t, utils.IsProjectHaveChildren(project), "Project has no children") +} + +func TestIsProjectHaveChildrenWithChildren(t *testing.T) { + project := &models.ProjectOutputDetailed{} + project.Foundation = nil + child := &models.ProjectOutput{ + ProjectCommon: models.ProjectCommon{}, + CreatedDate: nil, + DocuSignStatus: nil, + EndDate: nil, + EntityType: "", + ExecutiveDirector: nil, + Foundation: nil, + HerokuConnectID: "", + ID: testProjectID, + IsDeleted: false, + LFSponsored: false, + LegalParent: nil, + ModifiedDate: nil, + OpportunityOwner: nil, + Owner: nil, + ProgramManager: nil, + ProjectType: "SubProject", + RenewalOwner: nil, + Slug: "random-slug", + SystemModStamp: strfmt.DateTime{}, + Type: "", + } + project.Projects = []*models.ProjectOutput{child} + assert.True(t, utils.IsProjectHaveChildren(project), "Project has Children") +} diff --git a/cla-backend-go/tests/project_service_test.go b/cla-backend-go/tests/project_service_test.go new file mode 100644 index 000000000..1b18f47bc --- /dev/null +++ b/cla-backend-go/tests/project_service_test.go @@ -0,0 +1,36 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package tests + +import ( + "os" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/communitybridge/easycla/cla-backend-go/config" + "github.com/communitybridge/easycla/cla-backend-go/token" + "github.com/communitybridge/easycla/cla-backend-go/utils" + project_service "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" + "github.com/stretchr/testify/assert" +) + +var functionalTestEnabled = false + +func TestProjectServiceSummary(t *testing.T) { + if functionalTestEnabled { // nolint + var awsSession = session.Must(session.NewSession(&aws.Config{})) + stage := os.Getenv("STAGE") + assert.NotEmpty(t, stage) + configFile, err := config.LoadConfig("", awsSession, stage) + assert.Nil(t, err, "load config error") + token.Init(configFile.Auth0Platform.ClientID, configFile.Auth0Platform.ClientSecret, configFile.Auth0Platform.URL, configFile.Auth0Platform.Audience) + project_service.InitClient(configFile.PlatformAPIGatewayURL) + + client := project_service.GetClient() + projectSummaryModel, err := client.GetSummary(utils.NewContext(), "a096s000000VluyAAC") + assert.Nil(t, err, "Error is nil") + assert.NotNil(t, projectSummaryModel, "Project Summary Response not nil") + } +} diff --git a/cla-backend-go/tests/project_test.go b/cla-backend-go/tests/project_test.go index 927f91e69..9e215be0f 100644 --- a/cla-backend-go/tests/project_test.go +++ b/cla-backend-go/tests/project_test.go @@ -7,8 +7,9 @@ import ( "context" "testing" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" - "github.com/communitybridge/easycla/cla-backend-go/project" + "github.com/communitybridge/easycla/cla-backend-go/project/common" + + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/stretchr/testify/assert" ) @@ -56,7 +57,7 @@ func TestGetCurrentDocumentVersion(t *testing.T) { DocumentS3URL: "document3.pdf", }) - currentDoc, docErr := project.GetCurrentDocument(context.Background(), docs) + currentDoc, docErr := common.GetCurrentDocument(context.Background(), docs) assert.Nil(t, docErr, "current document error check is nil") assert.NotNil(t, currentDoc, "current document not nil") assert.Equal(t, "document3.pdf", currentDoc.DocumentS3URL, "loaded correct document") @@ -105,7 +106,7 @@ func TestGetCurrentDocumentDateTime(t *testing.T) { DocumentS3URL: "document3.pdf", }) - currentDoc, docErr := project.GetCurrentDocument(context.Background(), docs) + currentDoc, docErr := common.GetCurrentDocument(context.Background(), docs) assert.Nil(t, docErr, "current document error check is nil") assert.NotNil(t, currentDoc, "current document not nil") assert.Equal(t, "document3.pdf", currentDoc.DocumentS3URL, "loaded correct document") @@ -154,7 +155,7 @@ func TestGetCurrentDocumentDateTimeDiffOrder(t *testing.T) { DocumentS3URL: "document1.pdf", }) - currentDoc, docErr := project.GetCurrentDocument(context.Background(), docs) + currentDoc, docErr := common.GetCurrentDocument(context.Background(), docs) assert.Nil(t, docErr, "current document error check is nil") assert.NotNil(t, currentDoc, "current document not nil") assert.Equal(t, "document3.pdf", currentDoc.DocumentS3URL, "loaded correct document") @@ -203,7 +204,7 @@ func TestGetCurrentDocumentMixedUp(t *testing.T) { DocumentS3URL: "document3.pdf", }) - currentDoc, docErr := project.GetCurrentDocument(context.Background(), docs) + currentDoc, docErr := common.GetCurrentDocument(context.Background(), docs) assert.Nil(t, docErr, "current document error check is nil") assert.NotNil(t, currentDoc, "current document not nil") assert.Equal(t, "document1.pdf", currentDoc.DocumentS3URL, "loaded correct document") diff --git a/cla-backend-go/tests/repository_test.go b/cla-backend-go/tests/repository_test.go index 2207c4e95..723fb1029 100644 --- a/cla-backend-go/tests/repository_test.go +++ b/cla-backend-go/tests/repository_test.go @@ -7,7 +7,7 @@ package tests import ( "fmt" "github.com/aws/aws-sdk-go/aws" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" ini "github.com/communitybridge/easycla/cla-backend-go/init" "github.com/google/uuid" "github.com/stretchr/testify/assert" diff --git a/cla-backend-go/tests/signatures_test.go b/cla-backend-go/tests/signatures_test.go new file mode 100644 index 000000000..fac60faa7 --- /dev/null +++ b/cla-backend-go/tests/signatures_test.go @@ -0,0 +1,34 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package tests + +import ( + "testing" + + "github.com/communitybridge/easycla/cla-backend-go/signatures" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/stretchr/testify/assert" +) + +func TestCCLAInvalidateSignatureTemplate(t *testing.T) { + params := signatures.InvalidateSignatureTemplateParams{ + RecipientName: "CCLATest", + ClaType: utils.ClaTypeCCLA, + ClaManager: "claManager", + CLAGroupName: "claGroup test", + RemovalCriteria: "email removal", + CLAManagers: []signatures.ClaManagerInfoParams{ + {Username: "mgr_one", Email: "mgr_one_email"}, + {Username: "mgr_two", Email: "mgr_two_email"}, + }, + Company: "TestCompany", + } + + result, err := utils.RenderTemplate(utils.V2, signatures.InvalidateCCLASignatureTemplateName, signatures.InvalidateCCLASignatureTemplate, params) + assert.NoError(t, err) + assert.Contains(t, result, "This is a notification email from EasyCLA regarding the CLA Group claGroup test") + assert.Contains(t, result, "You were previously authorized to contribute on behalf of your company TestCompany under its CLA. However, a CLA Manager claManager has now removed you from the authorization list") + assert.Contains(t, result, "UserDetailsValue
") + assert.Contains(t, result, "Please notify the contributor once they are added so that they may complete the contribution process") + assert.Contains(t, result, `CLA for any of the project(s): Project1,Project2`) +} + +func TestV2CLAManagerDesigneeCorporateTemplate(t *testing.T) { + params := emails.V2CLAManagerDesigneeCorporateTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: "JohnsClaManager", + CompanyName: "JohnsCompany", + }, + CLAGroupTemplateParams: emails.CLAGroupTemplateParams{ + CLAGroupName: "JohnsCLAGroupName", + Projects: []emails.CLAProjectParams{{ + ExternalProjectName: "JohnsProject", + FoundationSFID: "FoundationSFIDValue", + ProjectSFID: "ProjectSFIDValue", + CorporateConsole: "http://CorporateConsole.com", + }}, + CorporateConsole: "http://CorporateConsole.com", + }, + SenderName: "SenderNameValue", + SenderEmail: "SenderEmailValue", + } + + result, err := emails.RenderTemplate(utils.V1, emails.V2CLAManagerDesigneeCorporateTemplateName, emails.V2CLAManagerDesigneeCorporateTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello JohnsClaManager") + assert.Contains(t, result, "CLA setup and signing process for the organization JohnsCompany") + assert.Contains(t, result, "SenderNameValue SenderEmailValue has identified you") + assert.Contains(t, result, "Corporate CLA for the organization JohnsCompany") + assert.Contains(t, result, "If you need help or have questions about EasyCLA, you can -read the documentation or + // We only support v2 help links as of late 2021/early2022 + helpLinkInfo := `
If you need help or have questions about EasyCLA, you can +read the documentation or reach out to us for support.
` + if showV2HelpLink { + return helpLinkInfo } - return `If you need help or have questions about EasyCLA, you can -read the documentation or -reach out to us for -support.
` + return helpLinkInfo } // GetEmailSignOffContent returns the standard email sign-off details func GetEmailSignOffContent() string { - return `Thanks,
-The LF Engineering Team
` + return `EasyCLA Support Team
` +} + +// RenderTemplate renders the template for given template with given params +func RenderTemplate(claGroupVersion, templateName, templateStr string, params interface{}) (string, error) { + tmpl := template.New(templateName) + t, err := tmpl.Parse(templateStr) + if err != nil { + return "", err + } + + var tpl bytes.Buffer + if err := t.Execute(&tpl, params); err != nil { + return "", err + } + + result := tpl.String() + result = result + GetEmailHelpContent(claGroupVersion == V2) + result = result + GetEmailSignOffContent() + return result, nil } diff --git a/cla-backend-go/utils/errors.go b/cla-backend-go/utils/errors.go index 04fa0e205..d93b59f5f 100644 --- a/cla-backend-go/utils/errors.go +++ b/cla-backend-go/utils/errors.go @@ -3,7 +3,10 @@ package utils -import "fmt" +import ( + "fmt" + "strings" +) // ConversionError is an error model for representing conversion errors type ConversionError struct { @@ -63,6 +66,40 @@ func (e *CLAGroupNotFound) Unwrap() error { return e.Err } +// ProjectSummary is a quick data model for the project name and ID +type ProjectSummary struct { + ID string + Name string +} + +// ProjectConflict is an error model for project conflict +type ProjectConflict struct { + Message string + ProjectA ProjectSummary + ProjectB ProjectSummary + Err error +} + +// Error is an error string function for CLA Group not found errors +func (e *ProjectConflict) Error() string { + msg := fmt.Sprintf("%s - ", e.Message) + msg = fmt.Sprintf("%s conflict between project %s (%s) and project %s (%s)", + msg, + e.ProjectA.Name, e.ProjectA.ID, + e.ProjectB.Name, e.ProjectB.ID, + ) + if e.Err == nil { + return strings.TrimSpace(msg) + } + + return strings.TrimSpace(fmt.Sprintf("%s, error: %+v", msg, e.Err)) +} + +// Unwrap method returns its contained error +func (e *ProjectConflict) Unwrap() error { + return e.Err +} + // CLAGroupNameConflict is an error model for CLA Group name conflicts type CLAGroupNameConflict struct { CLAGroupID string @@ -153,37 +190,6 @@ func (e *ProjectCLAGroupMappingNotFound) Unwrap() error { return e.Err } -// CompanyDoesNotExist is an error model for company does not exist errors -type CompanyDoesNotExist struct { - CompanyName string - CompanyID string - CompanySFID string - Err error -} - -// Error is an error string function for company does not exist errs -func (e *CompanyDoesNotExist) Error() string { - var errMsg = "company does not exist" - if e.CompanyName == "" { - errMsg = fmt.Sprintf("%s company name: %s", errMsg, e.CompanyName) - } - if e.CompanyID == "" { - errMsg = fmt.Sprintf("%s company id: %s", errMsg, e.CompanyID) - } - if e.CompanySFID == "" { - errMsg = fmt.Sprintf("%s company sfid: %s", errMsg, e.CompanySFID) - } - if e.Err != nil { - errMsg = fmt.Sprintf("%s error: %+v", errMsg, e.Err) - } - return errMsg -} - -// Unwrap method returns its contained error -func (e *CompanyDoesNotExist) Unwrap() error { - return e.Err -} - // GitHubOrgNotFound is an error model for GitHub Organization not found errors type GitHubOrgNotFound struct { ProjectSFID string @@ -207,7 +213,7 @@ type CompanyAdminNotFound struct { Err error } -// Error is an error string function for Salesforce Project not found errors +// Error is an error string function for the CompanyAdminNotFound model func (e *CompanyAdminNotFound) Error() string { if e.Err == nil { return fmt.Sprintf("company admin for company with ID %s not found", e.CompanySFID) @@ -219,3 +225,350 @@ func (e *CompanyAdminNotFound) Error() string { func (e *CompanyAdminNotFound) Unwrap() error { return e.Err } + +// UserNotFound is an error model for users not found errors +type UserNotFound struct { + Message string + UserLFID string + UserName string + UserEmail string + Err error +} + +// Error is an error string function for the CompanyNotFound model +func (e *UserNotFound) Error() string { + msg := "user does not exist " + if e.Message != "" { + msg = e.Message + } + if e.UserLFID != "" { + msg = fmt.Sprintf("%s - user LFID: %s ", msg, e.UserLFID) + } + if e.UserName != "" { + msg = fmt.Sprintf("%s - user name: %s ", msg, e.UserName) + } + if e.UserEmail != "" { + msg = fmt.Sprintf("%s - email: %s ", msg, e.UserEmail) + } + if e.Err != nil { + msg = fmt.Sprintf("%s - error: %+v ", msg, e.Err.Error()) + } + + return strings.TrimSpace(msg) +} + +// Unwrap method returns its contained error +func (e *UserNotFound) Unwrap() error { + return e.Err +} + +// CompanyNotFound is an error model for company not found errors +type CompanyNotFound struct { + Message string + CompanyID string + CompanySFID string + CompanyName string + CompanySigningEntityName string + Err error +} + +// Error is an error string function for the CompanyNotFound model +func (e *CompanyNotFound) Error() string { + msg := "company does not exist " + if e.Message != "" { + msg = e.Message + } + if e.CompanyName != "" { + msg = fmt.Sprintf("%s - company name: %s ", msg, e.CompanyName) + } + if e.CompanySigningEntityName != "" { + msg = fmt.Sprintf("%s - company sigining entity name: %s ", msg, e.CompanySigningEntityName) + } + if e.CompanyID != "" { + msg = fmt.Sprintf("%s - company ID: %s ", msg, e.CompanyID) + } + if e.CompanySFID != "" { + msg = fmt.Sprintf("%s - company SFID: %s ", msg, e.CompanySFID) + } + if e.Err != nil { + msg = fmt.Sprintf("%s - error: %+v ", msg, e.Err.Error()) + } + + return strings.TrimSpace(msg) +} + +// Unwrap method returns its contained error +func (e *CompanyNotFound) Unwrap() error { + return e.Err +} + +// InvalidRepositoryTypeError is an error model for an invalid repository type +type InvalidRepositoryTypeError struct { + RepositoryType string + RepositoryName string + Err error +} + +// Error is an error string function for the InvalidRepositoryTypeError model +func (e *InvalidRepositoryTypeError) Error() string { + msg := "Invalid repository type" + if e.RepositoryType != "" { + msg = fmt.Sprintf("%s - type: %s ", msg, e.RepositoryType) + } + if e.RepositoryName != "" { + msg = fmt.Sprintf("%s - repository: %s ", msg, e.RepositoryName) + } + if e.Err != nil { + msg = fmt.Sprintf("%s - error: %+v ", msg, e.Err.Error()) + } + + return strings.TrimSpace(msg) +} + +// Unwrap method returns its contained error +func (e *InvalidRepositoryTypeError) Unwrap() error { + return e.Err +} + +// GitHubRepositoryNotFound is an error model for a GitHub repository not found +type GitHubRepositoryNotFound struct { + Message string + RepositoryName string + Err error +} + +// Error is an error string function for the GitHubRepositoryNotFound model +func (e *GitHubRepositoryNotFound) Error() string { + msg := GithubRepoNotFound + if e.Message != "" { + msg = e.Message + } + if e.RepositoryName != "" { + msg = fmt.Sprintf("%s - repository: %s ", msg, e.RepositoryName) + } + if e.Err != nil { + msg = fmt.Sprintf("%s - error: %+v ", msg, e.Err.Error()) + } + + return strings.TrimSpace(msg) +} + +// Unwrap method returns its contained error +func (e *GitHubRepositoryNotFound) Unwrap() error { + return e.Err +} + +// GitHubRepositoryExists is an error model for when a GitHub repository already exists +type GitHubRepositoryExists struct { + Message string + RepositoryName string + Err error +} + +// Error is an error string function for the GitHubRepositoryExists model +func (e *GitHubRepositoryExists) Error() string { + msg := GithubRepoNotFound + if e.Message != "" { + msg = e.Message + } + if e.RepositoryName != "" { + msg = fmt.Sprintf("%s - repository: %s ", msg, e.RepositoryName) + } + if e.Err != nil { + msg = fmt.Sprintf("%s - error: %+v ", msg, e.Err.Error()) + } + + return strings.TrimSpace(msg) +} + +// Unwrap method returns its contained error +func (e *GitHubRepositoryExists) Unwrap() error { + return e.Err +} + +// GitLabRepositoryNotFound is an error model for a GitLab repository not found +type GitLabRepositoryNotFound struct { + Message string + OrganizationName string + RepositoryName string + RepositoryExternalID int64 + ProjectSFID string + CLAGroupID string + Err error +} + +// Error is an error string function for the GitHubRepositoryNotFound model +func (e *GitLabRepositoryNotFound) Error() string { + msg := GitLabRepoNotFound + if e.Message != "" { + msg = e.Message + } + if e.OrganizationName != "" { + msg = fmt.Sprintf("%s - organization: %s ", msg, e.OrganizationName) + } + if e.RepositoryName != "" { + msg = fmt.Sprintf("%s - repository: %s ", msg, e.RepositoryName) + } + if e.RepositoryExternalID > 0 { + msg = fmt.Sprintf("%s - repository external ID: %d ", msg, e.RepositoryExternalID) + } + if e.ProjectSFID != "" { + msg = fmt.Sprintf("%s - project SFID: %s ", msg, e.ProjectSFID) + } + if e.CLAGroupID != "" { + msg = fmt.Sprintf("%s - CLA Group ID: %s ", msg, e.CLAGroupID) + } + if e.Err != nil { + msg = fmt.Sprintf("%s - error: %+v ", msg, e.Err.Error()) + } + + return strings.TrimSpace(msg) +} + +// Unwrap method returns its contained error +func (e *GitLabRepositoryNotFound) Unwrap() error { + return e.Err +} + +// GitLabDuplicateRepositoriesFound is an error model for a GitLab duplicate repositories found +type GitLabDuplicateRepositoriesFound struct { + Message string + RepositoryName string + RepositoryExternalID int64 + Err error +} + +// Error is an error string function for the GitLabDuplicateRepositoriesFound model +func (e *GitLabDuplicateRepositoriesFound) Error() string { + msg := GitLabDuplicateRepoFound + if e.Message != "" { + msg = e.Message + } + if e.RepositoryName != "" { + msg = fmt.Sprintf("%s - repository: %s ", msg, e.RepositoryName) + } + if e.RepositoryExternalID > 0 { + msg = fmt.Sprintf("%s - repository external ID: %d ", msg, e.RepositoryExternalID) + } + if e.Err != nil { + msg = fmt.Sprintf("%s - error: %+v ", msg, e.Err.Error()) + } + + return strings.TrimSpace(msg) +} + +// Unwrap method returns its contained error +func (e *GitLabDuplicateRepositoriesFound) Unwrap() error { + return e.Err +} + +// GitLabRepositoryExists is an error model for when a GitHub repository already exists +type GitLabRepositoryExists struct { + Message string + RepositoryName string + Err error +} + +// Error is an error string function for the GitLabRepositoryExists model +func (e *GitLabRepositoryExists) Error() string { + msg := GitLabRepoNotFound + if e.Message != "" { + msg = e.Message + } + if e.RepositoryName != "" { + msg = fmt.Sprintf("%s - repository: %s ", msg, e.RepositoryName) + } + if e.Err != nil { + msg = fmt.Sprintf("%s - error: %+v ", msg, e.Err.Error()) + } + + return strings.TrimSpace(msg) +} + +// Unwrap method returns its contained error +func (e *GitLabRepositoryExists) Unwrap() error { + return e.Err +} + +// CLAManagerError is an error model for when a CLA Manager error occurs +type CLAManagerError struct { + Message string + Err error +} + +// Error is an error string function for the CLAManagerError model +func (e *CLAManagerError) Error() string { + msg := "CLA Manager Error" + if e.Message != "" { + msg = e.Message + } + if e.Err != nil { + msg = fmt.Sprintf("%s - error: %+v ", msg, e.Err.Error()) + } + + return strings.TrimSpace(msg) +} + +// Unwrap method returns its contained error +func (e *CLAManagerError) Unwrap() error { + return e.Err +} + +// InvalidCLAType is an error model for invalid CLA types, usually the CLA type is one of: utils.{ClaTypeICLA,ClaTypeECLA,ClaTypeCCLA} +type InvalidCLAType struct { + CLAType string + Err error +} + +// Error is an error string function for CLA Group not found errors +func (e *InvalidCLAType) Error() string { + if e.Err == nil { + return fmt.Sprintf("invalid CLA type: %s", e.CLAType) + } + return fmt.Sprintf("invalid CLA type: %s, %+v", e.CLAType, e.Err) +} + +// Unwrap method returns its contained error +func (e *InvalidCLAType) Unwrap() error { + return e.Err +} + +// EnrollError is an error model for representing enroll/un-enroll errors +type EnrollError struct { + Type string + Message string + Err error +} + +// Error is an error string function for enroll/un-enroll error +func (e *EnrollError) Error() string { + if e.Err == nil { + return fmt.Sprintf("%s validation error: %s", e.Type, e.Message) + } + return fmt.Sprintf("%s validation error: %s due to error: %+v", e.Type, e.Message, e.Err) +} + +// Unwrap method returns its contained error +func (e *EnrollError) Unwrap() error { + return e.Err +} + +// EnrollValidationError is an error model for representing enroll/un-enroll validation errors +type EnrollValidationError struct { + Type string + Message string + Err error +} + +// Error is an error string function for enroll/un-enroll validation error +func (e *EnrollValidationError) Error() string { + if e.Err == nil { + return fmt.Sprintf("%s validation error: %s", e.Type, e.Message) + } + return fmt.Sprintf("%s validation error: %s due to error: %+v", e.Type, e.Message, e.Err) +} + +// Unwrap method returns its contained error +func (e *EnrollValidationError) Unwrap() error { + return e.Err +} diff --git a/cla-backend-go/utils/events.go b/cla-backend-go/utils/events.go index fe15e5fd5..8ae8eb0bb 100644 --- a/cla-backend-go/utils/events.go +++ b/cla-backend-go/utils/events.go @@ -79,7 +79,7 @@ func ToEmailTemplateEvent(sender *string, recipients []string, subject *string, emailRecipients[i] = strfmt.Email(recipient) } - log.Debug("Generating email template event...") + // log.Debug("Generating email template event...") _, nowAsString := CurrentTime() from := strfmt.Email(*sender) return &emailevent.EmailEvent{ diff --git a/cla-backend-go/utils/lambda.go b/cla-backend-go/utils/lambda.go new file mode 100644 index 000000000..b680cb2c6 --- /dev/null +++ b/cla-backend-go/utils/lambda.go @@ -0,0 +1,26 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package utils + +import ( + "context" + "fmt" + + "github.com/aws/aws-lambda-go/events" +) + +// GetHTTPOKResponse : return Get HTTP Success Response +func GetHTTPOKResponse(ctx context.Context) events.APIGatewayProxyResponse { + resp := events.APIGatewayProxyResponse{ + StatusCode: 200, + IsBase64Encoded: false, + Body: "", + Headers: map[string]string{ + "Content-Type": "application/json", + XREQUESTID: fmt.Sprintf("%+v", ctx.Value(XREQUESTID)), + "X-MyCompany-Func-Reply": "hello-handler", + }, + } + return resp +} diff --git a/cla-backend-go/utils/list_utils.go b/cla-backend-go/utils/list_utils.go new file mode 100644 index 000000000..999a98a11 --- /dev/null +++ b/cla-backend-go/utils/list_utils.go @@ -0,0 +1,18 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package utils + +// FindInt64Duplicates returns true if the two lists include any duplicates, false otherwise. Returns the duplicates +func FindInt64Duplicates(a, b []int64) []int64 { + var duplicates []int64 + for i := 0; i < len(a); i++ { + for j := 0; j < len(b); j++ { + if a[i] == b[j] { + duplicates = append(duplicates, a[i]) + } + } + } + + return duplicates +} diff --git a/cla-backend-go/utils/project_helpers.go b/cla-backend-go/utils/project_helpers.go new file mode 100644 index 000000000..f1790c252 --- /dev/null +++ b/cla-backend-go/utils/project_helpers.go @@ -0,0 +1,43 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package utils + +import "github.com/communitybridge/easycla/cla-backend-go/v2/project-service/models" + +// GetProjectParentSFID returns the project parent SFID if available, otherwise returns empty string +func GetProjectParentSFID(project *models.ProjectOutputDetailed) string { + if project == nil || project.Foundation == nil || project.Foundation.ID == "" || project.Foundation.Name == "" || project.Foundation.Slug == "" { + return "" + } + return project.Foundation.ID +} + +// IsProjectHaveParent returns true if the specified project has a parent +func IsProjectHaveParent(project *models.ProjectOutputDetailed) bool { + return project != nil && project.Foundation != nil && project.Foundation.ID != "" && project.Foundation.Name != "" && project.Foundation.Slug != "" +} + +// IsProjectHasRootParent determines if a given project has a root parent. A root parent is a parent that is empty parent or the parent is TLF or LFProjects +func IsProjectHasRootParent(project *models.ProjectOutputDetailed) bool { + return project.Foundation == nil || (project.Foundation != nil && project.Foundation.ID != "" && (project.Foundation.Name == TheLinuxFoundation)) +} + +// IsStandaloneProject determines if a given project is a standalone project. A standalone project is a project with no parent or the parent is TLF/LFProjects and does not have any children +func IsStandaloneProject(project *models.ProjectOutputDetailed) bool { + // standalone: No parent or parent is TLF/LFProjects....and no children + return (project.Foundation == nil || + (project.Foundation != nil && (project.Foundation.Name == TheLinuxFoundation))) && + len(project.Projects) == 0 +} + +// IsProjectHaveChildren determines if a given project has children +func IsProjectHaveChildren(project *models.ProjectOutputDetailed) bool { + // a project model with a project list means it has children + return len(project.Projects) > 0 +} + +// IsProjectCategory determines if a given project is categorised as cla project sfid +func IsProjectCategory(project *models.ProjectOutputDetailed, parent *models.ProjectOutputDetailed) bool { + return project.ProjectType == ProjectTypeProject || (!IsProjectHasRootParent(project) && parent.ProjectType == ProjectTypeProjectGroup && project.ProjectType == ProjectTypeProjectGroup) +} diff --git a/cla-backend-go/utils/properties.go b/cla-backend-go/utils/properties.go new file mode 100644 index 000000000..410759e69 --- /dev/null +++ b/cla-backend-go/utils/properties.go @@ -0,0 +1,28 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package utils + +import ( + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +// GetProperty is a common routine to bind and return the specified environment variable +func GetProperty(property string) string { + f := logrus.Fields{ + "functionName": "utils.properties.GetProperty", + } + err := viper.BindEnv(property) + if err != nil { + log.WithFields(f).WithError(err).Fatalf("unable to load property: %s - value not defined or empty", property) + } + + value := viper.GetString(property) + if value == "" { + log.WithFields(f).WithError(err).Fatalf("property: %s cannot be empty", property) + } + + return value +} diff --git a/cla-backend-go/utils/regex.go b/cla-backend-go/utils/regex.go index e83d9053f..c00e725e8 100644 --- a/cla-backend-go/utils/regex.go +++ b/cla-backend-go/utils/regex.go @@ -28,3 +28,18 @@ func ValidWebsite(website string) bool { r := regexp.MustCompile(`^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$`) return r.MatchString(website) } + +// ParseString parses a string and returns group values defined in the regex +func ParseString(regEx, val string) (paramsMap map[string]string) { + + var compRegEx = regexp.MustCompile(regEx) + match := compRegEx.FindStringSubmatch(val) + + paramsMap = make(map[string]string) + for i, name := range compRegEx.SubexpNames() { + if i > 0 && i <= len(match) { + paramsMap[name] = match[i] + } + } + return paramsMap +} diff --git a/cla-backend-go/utils/responses.go b/cla-backend-go/utils/responses.go index df251d996..2ecb9452e 100644 --- a/cla-backend-go/utils/responses.go +++ b/cla-backend-go/utils/responses.go @@ -6,7 +6,7 @@ package utils import ( "fmt" - v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/models" + v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" ) @@ -37,6 +37,24 @@ func ErrorResponseBadRequestWithError(reqID, msg string, err error) *models.Erro } } +// ErrorResponseUnauthorized Helper function to generate an unauthorized error response +func ErrorResponseUnauthorized(reqID, msg string) *models.ErrorResponse { + return &models.ErrorResponse{ + Code: String401, + Message: fmt.Sprintf("%s - %s", EasyCLA401Unauthorized, msg), + XRequestID: reqID, + } +} + +// ErrorResponseUnauthorizedWithError Helper function to generate an unauthorized error response +func ErrorResponseUnauthorizedWithError(reqID, msg string, err error) *models.ErrorResponse { + return &models.ErrorResponse{ + Code: String401, + Message: fmt.Sprintf("%s - %s - error: %+v", EasyCLA401Unauthorized, msg, err), + XRequestID: reqID, + } +} + // ErrorResponseForbidden Helper function to generate a forbidden error response func ErrorResponseForbidden(reqID, msg string) *models.ErrorResponse { return &models.ErrorResponse{ @@ -73,6 +91,24 @@ func ErrorResponseNotFoundWithError(reqID, msg string, err error) *models.ErrorR } } +// ErrorResponseConflict Helper function to generate a conflict error response +func ErrorResponseConflict(reqID, msg string) *models.ErrorResponse { + return &models.ErrorResponse{ + Code: String409, + Message: fmt.Sprintf("%s - %s", EasyCLA409Conflict, msg), + XRequestID: reqID, + } +} + +// ErrorResponseConflictWithError Helper function to generate a conflict error message +func ErrorResponseConflictWithError(reqID, msg string, err error) *models.ErrorResponse { + return &models.ErrorResponse{ + Code: String409, + Message: fmt.Sprintf("%s - %s - error: %+v", EasyCLA409Conflict, msg, err), + XRequestID: reqID, + } +} + // ErrorResponseInternalServerError Helper function to generate an internal server error response func ErrorResponseInternalServerError(reqID, msg string) *models.ErrorResponse { return &models.ErrorResponse{ diff --git a/cla-backend-go/utils/s3.go b/cla-backend-go/utils/s3.go index 804de7fde..18ed627e8 100644 --- a/cla-backend-go/utils/s3.go +++ b/cla-backend-go/utils/s3.go @@ -6,11 +6,14 @@ package utils import ( "bytes" "errors" - "io/ioutil" + "io" + "os" "strings" "time" + "github.com/aws/aws-sdk-go-v2/service/s3/types" log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/sirupsen/logrus" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" @@ -23,9 +26,11 @@ const PresignedURLValidity = 15 * time.Minute // S3Storage provides methods to handle s3 storage type S3Storage interface { Upload(fileContent []byte, projectID string, claType string, identifier string, signatureID string) error + UploadFile(file *os.File, projectID string, claType string, identifier string, signatureID string) error Download(filename string) ([]byte, error) Delete(filename string) error GetPresignedURL(filename string) (string, error) + KeyExists(key string) (bool, error) } var s3Storage S3Storage @@ -57,6 +62,16 @@ func (s3c *S3Client) Upload(fileContent []byte, projectID string, claType string return err } +func (s3c *S3Client) UploadFile(file *os.File, projectID string, claType string, identifier string, signatureID string) error { + filename := strings.Join([]string{"contract-group", projectID, claType, identifier, signatureID}, "/") + ".pdf" + _, err := s3c.s3.PutObject(&s3.PutObjectInput{ + Bucket: aws.String(s3c.BucketName), + Key: aws.String(filename), + Body: file, + }) + return err +} + // Download file from s3 func (s3c *S3Client) Download(filename string) ([]byte, error) { ou, err := s3c.s3.GetObject(&s3.GetObjectInput{ @@ -69,7 +84,7 @@ func (s3c *S3Client) Download(filename string) ([]byte, error) { return nil, err } - body, err := ioutil.ReadAll(ou.Body) + body, err := io.ReadAll(ou.Body) if err != nil { log.Warnf("problem reading file from s3 bucket: %s resource: %s, error: %+v", s3c.BucketName, filename, err) @@ -111,6 +126,24 @@ func UploadToS3(body []byte, projectID string, claType string, identifier string return s3Storage.Upload(body, projectID, claType, identifier, signatureID) } +// UploadFileToS3 uploads file to s3 storage at path contract-group/Hello %s,
-This is a notification email from EasyCLA regarding the organization %s.
-The following contributor would like to submit a contribution to the %s CLA Group - and is requesting to be approved as a contributor for your organization:
-%s
-Approval can be done at %s
-Please notify the contributor once they are added so that they may complete the contribution process.
- %s - %s`, - manager, company, claGroupName, getFormattedUserDetails(userModel), lfxPortalURL, - utils.GetEmailHelpContent(true), utils.GetEmailSignOffContent()) - err := utils.SendEmail(subject, body, recipients) - if err != nil { - log.Warnf("problem sending email with subject: %s to recipients: %+v, error: %+v", subject, recipients, err) - } else { - log.Debugf("sent email with subject: %s to recipients: %+v", subject, recipients) + log.Debugf("Sending notification emails to CLA Managers: %+v", notifyCLAManagers.List) + for _, claManager := range notifyCLAManagers.List { + s.SendEmailToCLAManager(ctx, &EmailToCLAManagerModel{ + Contributor: userModel, + CLAManagerName: claManager.Name, + CLAManagerEmail: claManager.Email.String(), + CompanyName: notifyCLAManagers.CompanyName, + CorporateConsoleURL: CorporateConsoleV2URL, + }, projectSFIDs) } + + return nil } // getBestUserName is a helper function to extract what information we can from the user record for purposes of displaying the user's name @@ -1102,44 +1216,48 @@ func getBestUserName(model *v1Models.User) string { return "User Name Unknown" } -func getContributorPublicEmail(model *v1User.User) (string, string) { - var contributorUserName, contributorEmail string +func getContributorPublicEmail(model *v1User.User) emails.Contributor { + var contributorModel emails.Contributor if model.LFUsername != "" { - contributorUserName = model.LFUsername + contributorModel.Username = model.LFUsername + contributorModel.UsernameLabel = utils.UserLabel } if model.LFEmail != "" { - contributorEmail = model.LFEmail + contributorModel.Email = model.LFEmail + contributorModel.EmailLabel = utils.EmailLabel } - if contributorUserName == "" { - contributorUserName = model.UserGithubUsername + if contributorModel.Username == "" { + contributorModel.Username = model.UserGithubUsername + contributorModel.UsernameLabel = utils.GitHubUserLabel } - if contributorEmail == "" && len(model.UserEmails) > 0 { + if contributorModel.Email == "" && len(model.UserEmails) > 0 { for _, email := range model.UserEmails { if strings.Contains(email, "users.noreply.github.com") { continue } - contributorEmail = email + contributorModel.Email = email + contributorModel.EmailLabel = utils.GitHubEmailLabel } } - return contributorUserName, contributorEmail + return contributorModel } // getFormattedUserDetails is a helper function to extract what information we can from the user record for purposes of displaying the user's information func getFormattedUserDetails(model *v1Models.User) string { var details []string if model.Username != "" { - details = append(details, fmt.Sprintf("User Name: %s", model.Username)) + details = append(details, fmt.Sprintf("Name/User Name: %s", model.Username)) } if model.GithubUsername != "" { details = append(details, fmt.Sprintf("GitHub User Name: %s", model.GithubUsername)) } - if model.GithubID != "" { - details = append(details, fmt.Sprintf("GitHub ID: %s", model.GithubID)) + if model.GitlabUsername != "" { + details = append(details, fmt.Sprintf("GitLab User Name: %s", model.GitlabUsername)) } if model.LfUsername != "" { @@ -1154,13 +1272,13 @@ func getFormattedUserDetails(model *v1Models.User) string { details = append(details, fmt.Sprintf("Emails: %s", strings.Join(model.Emails, ", "))) } - return strings.Join(details, ",") + return strings.Join(details, ", ") } // isSigned is a helper function to check if project/claGroup is signed func (s *service) isSigned(ctx context.Context, companyModel *v1Models.Company, projectID string) (bool, error) { f := logrus.Fields{ - "functionName": "isSigned", + "functionName": "cla_manager.service.isSigned", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "companyID": companyModel.CompanyID, "companyName": companyModel.CompanyName, @@ -1171,7 +1289,7 @@ func (s *service) isSigned(ctx context.Context, companyModel *v1Models.Company, f["companyID"] = companyModel.CompanyID f["companyName"] = companyModel.CompanyName log.WithFields(f).Debug("loading CLA Managers for company/project") - claManagers, err := s.v2CompanyService.GetCompanyProjectCLAManagers(ctx, companyModel.CompanyID, companyModel.CompanyExternalID, projectID) + claManagers, err := s.v2CompanyService.GetCompanyProjectCLAManagers(ctx, companyV1toV2(companyModel), projectID) if err != nil { msg := fmt.Sprintf("EasyCLA - 400 Bad Request : %v", err) log.WithFields(f).Warn(msg) @@ -1186,254 +1304,47 @@ func (s *service) isSigned(ctx context.Context, companyModel *v1Models.Company, return false, nil } -func projectsStrList(projectNames []string) string { - var sb strings.Builder - sb.WriteString("Hello %s,
-This is a notification email from EasyCLA regarding the CLA setup and signing process for %s.
-%s %s has identified you as a potential candidate to setup the Corporate CLA for %s in support of the following projects:
-%s -Before the contribution can be accepted, your organization must sign a CLA. -Either you or someone whom to designate from your company can login to this portal (%s) and sign the CLA for this project %s
-If you are not the CLA Manager, please forward this email to the appropriate person so that they can start the CLA process.
-Please notify the user once CLA setup is complete.
-%s -%s`, - admin, company, senderName, senderEmail, company, projectList, corporateConsole, projectNames[0], - utils.GetEmailHelpContent(true), utils.GetEmailSignOffContent()) - - err := utils.SendEmail(subject, body, recipients) - if err != nil { - log.Warnf("problem sending email with subject: %s to recipients: %+v, error: %+v", subject, recipients, err) - } else { - log.Debugf("sent email with subject: %s to recipients: %+v", subject, recipients) - } -} - -func contributorEmailToOrgAdmin(adminEmail string, admin string, company string, projectNames []string, contributor *v1Models.User, corporateConsole string) { - subject := fmt.Sprintf("EasyCLA: Invitation to Sign the %s Corporate CLA and add to approved list %s ", company, getBestUserName(contributor)) - recipients := []string{adminEmail} - body := fmt.Sprintf(` -Hello %s,
-This is a notification email from EasyCLA regarding the project(s) %s.
-The following contributor is requesting to sign CLA for organization:
-%s
-Before the user contribution can be accepted, your organization must sign a CLA. -
Kindly login to this portal %s and sign the CLA for any of the projects %s.
-Please notify the contributor once they are added so that they may complete the contribution process.
-%s -%s`, - admin, projectNames, getFormattedUserDetails(contributor), corporateConsole, projectNames, - utils.GetEmailHelpContent(true), utils.GetEmailSignOffContent()) - - err := utils.SendEmail(subject, body, recipients) - if err != nil { - log.Warnf("problem sending email with subject: %s to recipients: %+v, error: %+v", subject, recipients, err) - } else { - log.Debugf("sent email with subject: %s to recipients: %+v", subject, recipients) - } -} - -func sendEmailToCLAManagerDesigneeCorporate(ctx context.Context, corporateConsole string, companyName string, projectNames []string, designeeEmail string, designeeName string, senderEmail string, senderName string) { - f := logrus.Fields{ - "functionName": "sendEmailToCLAManagerDesigneeCorporate", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "corporateConsole": corporateConsole, - "companyName": companyName, - "projectNames": strings.Join(projectNames, ","), - "designeeEmail": designeeEmail, - "designeeName": designeeName, - "senderEmail": senderEmail, - "senderName": senderName, - } - - subject := fmt.Sprintf("EasyCLA: Invitation to Sign the %s Corporate CLA ", companyName) - recipients := []string{designeeEmail} - projectList := projectsStrList(projectNames) - body := fmt.Sprintf(` -Hello %s,
-This is a notification email from EasyCLA regarding the CLA setup and signing process for %s.
-%s %s has identified you as a potential candidate to setup the Corporate CLA for %s in support of the following projects:
-%s -Before the contribution can be accepted, your organization must sign a CLA. -Either you or someone whom to designate from your company can login to this portal (%s) and sign the CLA for this project %s
-If you are not the CLA Manager, please forward this email to the appropriate person so that they can start the CLA process.
-Please notify the user once CLA setup is complete.
-%s -%s`, - designeeName, companyName, senderName, senderEmail, companyName, projectList, corporateConsole, projectNames[0], - utils.GetEmailHelpContent(true), utils.GetEmailSignOffContent()) - - err := utils.SendEmail(subject, body, recipients) - if err != nil { - log.WithFields(f).WithError(err).Warnf("problem sending email with subject: %s to recipients: %+v, error: %+v", subject, recipients, err) - } else { - log.WithFields(f).Debugf("sent email with subject: %s to recipients: %+v", subject, recipients) - } +// buildErrorMessage helper function to build an error message +func buildErrorMessage(errPrefix string, claGroupID string, params cla_manager.CreateCLAManagerParams, err error) string { + return fmt.Sprintf("%s - problem creating new CLA Manager Request using company ID: %s, project ID: %s, first name: %s, last name: %s, user email: %s, error: %+v", + errPrefix, params.CompanyID, claGroupID, *params.Body.FirstName, *params.Body.LastName, *params.Body.UserEmail, err) } -func sendEmailToCLAManagerDesignee(ctx context.Context, corporateConsole string, companyName string, projectNames []string, designeeEmail string, designeeName string, contributorID string, contributorName string) { - f := logrus.Fields{ - "functionName": "sendEmailToCLAManagerDesignee", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "corporateConsole": corporateConsole, - "companyName": companyName, - "projectNames": strings.Join(projectNames, ","), - "designeeEmail": designeeEmail, - "designeeName": designeeName, - "contributorID": contributorID, - "contributorName": contributorName, - } - - subject := fmt.Sprintf("EasyCLA: Invitation to Sign the %s Corporate CLA and add to approved list %s ", - companyName, contributorID) - recipients := []string{designeeEmail} - body := fmt.Sprintf(` -Hello %s,
-This is a notification email from EasyCLA regarding the project(s) %s.
-The following contributor is requesting to sign CLA for organization:
-%s (%s)
-Before the user contribution can be accepted, your organization must sign a CLA. -
Kindly login to this portal %s and sign the CLA for one of the project(s) %s.
-Please notify the contributor once they are added so that they may complete the contribution process.
-%s -%s`, - designeeName, projectNames, contributorID, contributorName, corporateConsole, projectNames, - utils.GetEmailHelpContent(true), utils.GetEmailSignOffContent()) - - err := utils.SendEmail(subject, body, recipients) - if err != nil { - log.WithFields(f).WithError(err).Warnf("problem sending email with subject: %s to recipients: %+v, error: %+v", subject, recipients, err) - } else { - log.WithFields(f).Debugf("sent email with subject: %s to recipients: %+v", subject, recipients) +func companyV1toV2(v1CompanyModel *v1Models.Company) *models.Company { + return &models.Company{ + CompanyACL: v1CompanyModel.CompanyACL, + CompanyID: v1CompanyModel.CompanyID, + CompanyExternalID: v1CompanyModel.CompanyExternalID, + CompanyName: v1CompanyModel.CompanyName, + SigningEntityName: v1CompanyModel.SigningEntityName, + CompanyManagerID: v1CompanyModel.CompanyManagerID, + Note: v1CompanyModel.Note, + Created: v1CompanyModel.Created, + Updated: v1CompanyModel.Updated, + Version: v1CompanyModel.Version, } } -func sendDesigneeEmailToUserWithNoLFID(ctx context.Context, requesterUsername, requesterEmail, userWithNoLFIDName, userWithNoLFIDEmail, organizationName, organizationID, projectName string, projectID *string, role string) error { - f := logrus.Fields{ - "functionName": "sendDesigneeEmailToUserWithNoLFID", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "userWithNoLFIDName": userWithNoLFIDName, - "userWithNoLFIDEmail": userWithNoLFIDEmail, - "organizationID": organizationID, - "projectID": utils.StringValue(projectID), - "role": role, - } - - subject := fmt.Sprintf("EasyCLA: Invitation to create LF Login and complete process of becoming CLA Manager for project: %s ", projectName) - body := fmt.Sprintf(` -Hello %s,
-User %s (%s) was trying to add you as a CLA Manager for Project %s and Company %s but was unable to identify your account details in the EasyCLA system
-This email will guide you to completing the CLA Manager role assignment
-1. Accept Invite link below will take you SSO login page where you can login with your LF Login or create a LF Login and then login.
-2. After logging in SSO screen should direct you to CLA Corporate Console page where you will see the project you a re associated with.
-3. Click on workflow steps to complete the signup process. Please follow this documentation to help you guide through the process - https://docs.linuxfoundation.org/lfx/easycla/ccla-managers-and-ccla-signatories
-4. Once you have completed CLA Manager workflow you will be able to manage the approved list of contributors
- - %s - %s - `, userWithNoLFIDName, requesterUsername, requesterEmail, organizationName, projectName, - utils.GetEmailHelpContent(true), utils.GetEmailSignOffContent()) - acsClient := v2AcsService.GetClient() - automate := false - log.WithFields(f).Debug("sending user invite request...") - return acsClient.SendUserInvite(ctx, &userWithNoLFIDEmail, role, "project|organization", projectID, organizationID, "userinvite", &subject, &body, automate) - -} - -// sendEmailToUserWithNoLFID helper function to send email to a given user with no LFID -func sendEmailToUserWithNoLFID(ctx context.Context, projectName, requesterUsername, requesterEmail, userWithNoLFIDName, userWithNoLFIDEmail, organizationID string, projectID *string, role string) error { - f := logrus.Fields{ - "functionName": "sendEmailToUserWithNoLFID", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "projectName": projectName, - "requesterUsername": requesterUsername, - "requesterEmail": requesterEmail, - "userWithNoLFIDName": userWithNoLFIDName, - "userWithNoLFIDEmail": userWithNoLFIDEmail, - "organizationID": organizationID, - "projectID": utils.StringValue(projectID), - "role": role, - } - - // subject string, body string, recipients []string - subject := fmt.Sprintf("EasyCLA: Invitation to create LF Login and complete process of becoming CLA Manager with %s role", role) - body := fmt.Sprintf(` -Hello %s,
-This is a notification email from EasyCLA regarding the Project %s in the EasyCLA system.
-User %s (%s) was trying to add you as a CLA Manager for Project %s but was unable to identify your account details in -the EasyCLA system. In order to become a CLA Manager for Project %s, you will need to accept invite below. -Once complete, notify the user %s and they will be able to add you as a CLA Manager.
- -%s -%s`, - userWithNoLFIDName, projectName, - requesterUsername, requesterEmail, projectName, projectName, - requesterUsername, - utils.GetEmailHelpContent(true), utils.GetEmailSignOffContent()) - - acsClient := v2AcsService.GetClient() - automate := false - - log.WithFields(f).Debug("sending user invite request...") - return acsClient.SendUserInvite(ctx, &userWithNoLFIDEmail, role, "project|organization", projectID, organizationID, "userinvite", &subject, &body, automate) -} - -// buildErrorMessage helper function to build an error message -func buildErrorMessage(errPrefix string, claGroupID string, params cla_manager.CreateCLAManagerParams, err error) string { - return fmt.Sprintf("%s - problem creating new CLA Manager Request using company SFID: %s, project ID: %s, first name: %s, last name: %s, user email: %s, error: %+v", - errPrefix, params.CompanySFID, claGroupID, *params.Body.FirstName, *params.Body.LastName, *params.Body.UserEmail, err) -} - -func (s *service) convertGHUserToContact(ctx context.Context, contributor *v1User.User) error { - f := logrus.Fields{ - "functionName": "convertGHUserToContact", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), +// GetNonNoReplyUserEmail tries to fetch an email which doesn't have noreply string in it +// but if it's the only one we have it'll still be returned +func GetNonNoReplyUserEmail(userEmails []string) string { + if len(userEmails) == 0 { + return "" } - userService := v2UserService.GetClient() - log.Infof("Checking if GH User: %s, GH ID: %s has LFID for contact conversion ", contributor.UserGithubUsername, contributor.UserGithubID) - var GHUserLF *v2UserModels.User - var GHUserErr error - if contributor.LFEmail != "" { - GHUserLF, GHUserErr = userService.SearchUserByEmail(contributor.LFEmail) - if GHUserErr != nil { - msg := fmt.Sprintf("GH UserEmail: %s has no LF Login ", contributor.LFEmail) - log.Warn(msg) - } + var excludedEmails []string - } else if contributor.LFUsername != "" { - GHUserLF, GHUserErr = userService.GetUserByUsername(contributor.LFUsername) - if GHUserErr != nil { - msg := fmt.Sprintf("GH Username: %s has no LF Login ", contributor.LFUsername) - log.Warn(msg) + for _, email := range userEmails { + if strings.HasSuffix(email, excludedNoReplyEmails) { + excludedEmails = append(excludedEmails, email) + continue } + return email } - if GHUserLF != nil { - // Convert user to contact - if GHUserLF.Type == utils.Lead { - // convert user to contact - log.WithFields(f).Debug("converting lead to contact") - err := userService.ConvertToContact(GHUserLF.ID) - if err != nil { - msg := fmt.Sprintf("converting lead to contact failed: %v", err) - log.WithFields(f).Warn(msg) - return err - } - } + if len(excludedEmails) > 0 { + return excludedEmails[0] } - return nil + + return "" } diff --git a/cla-backend-go/v2/cla_manager/service_test.go b/cla-backend-go/v2/cla_manager/service_test.go new file mode 100644 index 000000000..3d7c86357 --- /dev/null +++ b/cla-backend-go/v2/cla_manager/service_test.go @@ -0,0 +1,54 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package cla_manager + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetNonNoReplyUserEmail(t *testing.T) { + testCases := []struct { + name string + emails []string + resultEmail string + }{ + { + name: "empty emails", + emails: []string{}, + resultEmail: "", + }, + { + name: "single noreply email", + emails: []string{ + "single@users.noreply.github.com", + }, + resultEmail: "single@users.noreply.github.com", + }, + { + name: "multiple emails with noreply", + emails: []string{ + "single@users.noreply.github.com", + "pumacat@gmail.com", + }, + resultEmail: "pumacat@gmail.com", + }, + { + name: "multiple emails without noreply", + emails: []string{ + "pumacat@gmail.com", + "pumacat2@gmail.com", + }, + resultEmail: "pumacat@gmail.com", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(tt *testing.T) { + result := GetNonNoReplyUserEmail(tc.emails) + assert.Equal(tt, tc.resultEmail, result) + }) + } +} diff --git a/cla-backend-go/v2/common/models.go b/cla-backend-go/v2/common/models.go new file mode 100644 index 000000000..e78bfd2d8 --- /dev/null +++ b/cla-backend-go/v2/common/models.go @@ -0,0 +1,113 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package common + +import ( + models2 "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" +) + +// GitLabOrganization is data model for gitlab organizations +type GitLabOrganization struct { + OrganizationID string `json:"organization_id"` + ExternalGroupID int `json:"external_gitlab_group_id"` + DateCreated string `json:"date_created,omitempty"` + DateModified string `json:"date_modified,omitempty"` + OrganizationName string `json:"organization_name,omitempty"` + OrganizationNameLower string `json:"organization_name_lower,omitempty"` + OrganizationFullPath string `json:"organization_full_path,omitempty"` + OrganizationURL string `json:"organization_url,omitempty"` + OrganizationSFID string `json:"organization_sfid,omitempty"` + ProjectSFID string `json:"project_sfid"` + Enabled bool `json:"enabled"` + AutoEnabled bool `json:"auto_enabled"` + BranchProtectionEnabled bool `json:"branch_protection_enabled"` + AutoEnabledClaGroupID string `json:"auto_enabled_cla_group_id,omitempty"` + AuthInfo string `json:"auth_info"` + AuthState string `json:"auth_state"` + Note string `json:"note,omitempty"` + AuthExpirationTime int `json:"auth_expiry_time,omitempty"` + Version string `json:"version,omitempty"` +} + +// ToModel converts to models.GitlabOrganization +func ToModel(in *GitLabOrganization) *models2.GitlabOrganization { + return &models2.GitlabOrganization{ + AuthInfo: in.AuthInfo, + OrganizationID: in.OrganizationID, + DateCreated: in.DateCreated, + DateModified: in.DateModified, + OrganizationName: in.OrganizationName, + OrganizationFullPath: in.OrganizationFullPath, + OrganizationURL: in.OrganizationURL, + OrganizationSfid: in.OrganizationSFID, + Version: in.Version, + Enabled: in.Enabled, + AutoEnabled: in.AutoEnabled, + AutoEnabledClaGroupID: in.AutoEnabledClaGroupID, + BranchProtectionEnabled: in.BranchProtectionEnabled, + ProjectSfid: in.ProjectSFID, + OrganizationExternalID: int64(in.ExternalGroupID), + AuthState: in.AuthState, + AuthExpiryTime: int64(in.AuthExpirationTime), + } +} + +// ToCommonModel converts to common.GitLabOrganization +func ToCommonModel(in *models2.GitlabOrganization) *GitLabOrganization { + return &GitLabOrganization{ + AuthInfo: in.AuthInfo, + OrganizationID: in.OrganizationID, + DateCreated: in.DateCreated, + DateModified: in.DateModified, + OrganizationName: in.OrganizationName, + OrganizationFullPath: in.OrganizationFullPath, + OrganizationURL: in.OrganizationURL, + OrganizationSFID: in.OrganizationSfid, + Version: in.Version, + Enabled: in.Enabled, + AutoEnabled: in.AutoEnabled, + AutoEnabledClaGroupID: in.AutoEnabledClaGroupID, + BranchProtectionEnabled: in.BranchProtectionEnabled, + ProjectSFID: in.ProjectSfid, + ExternalGroupID: int(in.OrganizationExternalID), + AuthState: in.AuthState, + AuthExpirationTime: int(in.AuthExpiryTime), + } +} + +// ToModels converts a list of GitLab organizations to a list of external GitLab organization response models +func ToModels(input []*GitLabOrganization) []*models2.GitlabOrganization { + out := make([]*models2.GitlabOrganization, 0) + for _, in := range input { + out = append(out, ToModel(in)) + } + return out +} + +// GitLabAddOrganization is data model for GitLab add organization requests +type GitLabAddOrganization struct { + OrganizationID string `json:"organization_id"` + ExternalGroupID int64 `json:"external_gitlab_group_id"` + DateCreated string `json:"date_created,omitempty"` + DateModified string `json:"date_modified,omitempty"` + OrganizationName string `json:"organization_name,omitempty"` + OrganizationNameLower string `json:"organization_name_lower,omitempty"` + OrganizationFullPath string `json:"organization_full_path,omitempty"` + OrganizationURL string `json:"organization_url,omitempty"` + OrganizationSFID string `json:"organization_sfid,omitempty"` + ProjectSFID string `json:"project_sfid"` + ParentProjectSFID string `json:"parent_project_sfid"` + Enabled bool `json:"enabled"` + AutoEnabled bool `json:"auto_enabled"` + BranchProtectionEnabled bool `json:"branch_protection_enabled"` + AutoEnabledClaGroupID string `json:"auto_enabled_cla_group_id,omitempty"` + AuthInfo string `json:"auth_info"` + AuthState string `json:"auth_state"` + Version string `json:"version,omitempty"` +} + +// ExternalGroupIDAsInt returns the external group ID as an integer value +func (m *GitLabAddOrganization) ExternalGroupIDAsInt() int { + return int(m.ExternalGroupID) +} diff --git a/cla-backend-go/v2/company/handlers.go b/cla-backend-go/v2/company/handlers.go index 8bc0580f3..93c269e79 100644 --- a/cla-backend-go/v2/company/handlers.go +++ b/cla-backend-go/v2/company/handlers.go @@ -9,6 +9,8 @@ import ( "fmt" "strings" + organization_service "github.com/communitybridge/easycla/cla-backend-go/v2/organization-service" + "github.com/aws/aws-sdk-go/aws" log "github.com/communitybridge/easycla/cla-backend-go/logging" @@ -16,7 +18,6 @@ import ( "github.com/sirupsen/logrus" "github.com/LF-Engineering/lfx-kit/auth" - v1Company "github.com/communitybridge/easycla/cla-backend-go/company" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations/company" @@ -26,48 +27,128 @@ import ( ) // Configure sets up the middleware handlers -func Configure(api *operations.EasyclaAPI, service Service, v1CompanyRepo v1Company.IRepository, projectClaGroupRepo projects_cla_groups.Repository, LFXPortalURL, v1CorporateConsole string) { // nolint +func Configure(api *operations.EasyclaAPI, service Service, projectClaGroupRepo projects_cla_groups.Repository, LFXPortalURL, v1CorporateConsole string) { // nolint - const msgUnableToLoadCompany = "unable to load company external ID" - api.CompanyGetCompanyProjectClaManagersHandler = company.GetCompanyProjectClaManagersHandlerFunc( - func(params company.GetCompanyProjectClaManagersParams, authUser *auth.User) middleware.Responder { + api.CompanyGetCompanyByInternalIDHandler = company.GetCompanyByInternalIDHandlerFunc( + func(params company.GetCompanyByInternalIDParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "CompanyGetCompanyProjectClaManagersHandler", + "functionName": "v2.company.handlers.CompanyGetCompanyByInternalIDHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "projectSFID": params.ProjectSFID, - "companySFID": params.CompanySFID, + "companyID": params.CompanyID, + "authUserName": utils.StringValue(params.XUSERNAME), + "authUserEmail": utils.StringValue(params.XEMAIL), + } + + // Lookup the company by internal ID + log.WithFields(f).Debugf("looking up company by internal ID...") + v2CompanyModel, err := service.GetCompanyByID(ctx, params.CompanyID) + if err != nil { + msg := fmt.Sprintf("unable to lookup company by ID: %s", params.CompanyID) + log.WithFields(f).WithError(err).Warn(msg) + if _, ok := err.(*utils.CompanyNotFound); ok { + return company.NewGetCompanyByInternalIDNotFound().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFoundWithError(reqID, msg, err)) + } + return company.NewGetCompanyByInternalIDBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + + if v2CompanyModel == nil { + msg := fmt.Sprintf("unable to lookup company by ID: %s", params.CompanyID) + log.WithFields(f).WithError(err).Warn(msg) + return company.NewGetCompanyByInternalIDNotFound().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFound(reqID, msg)) } log.WithFields(f).Debug("checking permissions") - if !utils.IsUserAuthorizedForOrganization(authUser, params.CompanySFID, utils.ALLOW_ADMIN_SCOPE) { - return company.NewGetCompanyProjectClaManagersForbidden().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "403", - Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to Get Company Project CLA Managers with Organization scope of %s", - authUser.UserName, params.CompanySFID), - XRequestID: reqID, - }) - } - comp, err := v1CompanyRepo.GetCompanyByExternalID(ctx, params.CompanySFID) + if !utils.IsUserAuthorizedForOrganization(ctx, authUser, v2CompanyModel.CompanyExternalID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to CompanyGetCompanyByInternalIDHandler with Organization scope of %s", + authUser.UserName, v2CompanyModel.CompanyExternalID) + log.WithFields(f).Warn(msg) + return company.NewGetCompanyByInternalIDForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) + } + + return company.NewGetCompanyByInternalIDOK().WithXRequestID(reqID).WithPayload(v2CompanyModel) + }) + + api.CompanyGetCompanyByExternalIDHandler = company.GetCompanyByExternalIDHandlerFunc( + func(params company.GetCompanyByExternalIDParams, authUser *auth.User) middleware.Responder { + reqID := utils.GetRequestID(params.XREQUESTID) + ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + f := logrus.Fields{ + "functionName": "v2.company.handlers.CompanyGetCompanyByExternalIDHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companySFID": params.CompanySFID, + "authUserName": utils.StringValue(params.XUSERNAME), + "authUserEmail": utils.StringValue(params.XEMAIL), + } + + // Lookup the company by internal ID + log.WithFields(f).Debugf("looking up company by SFID...") + v2CompanyModel, err := service.GetCompanyBySFID(ctx, params.CompanySFID) if err != nil { - msg := "unable to load company by SFID" + msg := fmt.Sprintf("unable to lookup company by SFID: %s", params.CompanySFID) log.WithFields(f).WithError(err).Warn(msg) - if err == v1Company.ErrCompanyDoesNotExist { - return company.NewGetCompanyProjectClaManagersNotFound().WithXRequestID(reqID).WithPayload( - utils.ErrorResponseNotFoundWithError(reqID, msg, err)) + if _, ok := err.(*utils.CompanyNotFound); ok { + return company.NewGetCompanyByExternalIDNotFound().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFoundWithError(reqID, msg, err)) } - return company.NewGetCompanyProjectClaManagersNotFound().WithXRequestID(reqID).WithPayload( - utils.ErrorResponseNotFoundWithError(reqID, msg, err)) + if _, ok := err.(*organizations.GetOrgNotFound); ok { + return company.NewGetCompanyByExternalIDNotFound().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFoundWithError(reqID, msg, err)) + } + log.WithFields(f).Debugf("error type is: %T", err) + return company.NewGetCompanyByExternalIDBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } - if comp == nil { - log.WithFields(f).WithError(err).Warn(msgUnableToLoadCompany) - return company.NewGetCompanyProjectClaManagersNotFound().WithXRequestID(reqID).WithPayload( - utils.ErrorResponseNotFound(reqID, msgUnableToLoadCompany)) + + if v2CompanyModel == nil { + msg := fmt.Sprintf("unable to lookup company by SFID: %s", params.CompanySFID) + log.WithFields(f).WithError(err).Warn(msg) + return company.NewGetCompanyByExternalIDNotFound().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFound(reqID, msg)) + } + + log.WithFields(f).Debug("checking permissions") + if !utils.IsUserAuthorizedForOrganization(ctx, authUser, params.CompanySFID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to CompanyGetCompanyByExternalIDHandler with Organization scope of %s", + authUser.UserName, v2CompanyModel.CompanyExternalID) + log.WithFields(f).Warn(msg) + return company.NewGetCompanyByExternalIDForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } - result, err := service.GetCompanyProjectCLAManagers(ctx, comp.CompanyID, params.CompanySFID, params.ProjectSFID) + return company.NewGetCompanyByExternalIDOK().WithXRequestID(reqID).WithPayload(v2CompanyModel) + }) + + api.CompanyGetCompanyProjectClaManagersHandler = company.GetCompanyProjectClaManagersHandlerFunc( + func(params company.GetCompanyProjectClaManagersParams, authUser *auth.User) middleware.Responder { + reqID := utils.GetRequestID(params.XREQUESTID) + ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + f := logrus.Fields{ + "functionName": "v2.company.handlers.CompanyGetCompanyProjectClaManagersHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": params.ProjectSFID, + "companyID": params.CompanyID, + "authUserName": utils.StringValue(params.XUSERNAME), + "authUserEmail": utils.StringValue(params.XEMAIL), + } + + // Lookup the company by internal ID + log.WithFields(f).Debugf("looking up company by internal ID...") + v2CompanyModel, err := service.GetCompanyByID(ctx, params.CompanyID) + if err != nil || v2CompanyModel == nil { + msg := fmt.Sprintf("unable to lookup company by ID: %s", params.CompanyID) + log.WithFields(f).WithError(err).Warn(msg) + return company.NewGetCompanyProjectClaManagersBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + + log.WithFields(f).Debug("checking permissions") + if !utils.IsUserAuthorizedForOrganization(ctx, authUser, v2CompanyModel.CompanyExternalID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to GetCompanyProjectClaManagers with Project|Organization scope of %s | %s", + authUser.UserName, params.ProjectSFID, v2CompanyModel.CompanyExternalID) + log.WithFields(f).Warn(msg) + return company.NewGetCompanyProjectClaManagersForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) + } + + result, err := service.GetCompanyProjectCLAManagers(ctx, v2CompanyModel, params.ProjectSFID) if err != nil { msg := "unable to load company project CLA managers" log.WithFields(f).WithError(err).Warn(msg) @@ -84,7 +165,7 @@ func Configure(api *operations.EasyclaAPI, service Service, v1CompanyRepo v1Comp reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint f := logrus.Fields{ - "functionName": "CompanyGetCompanyCLAGroupManagersHandler", + "functionName": "v2.company.handlers.CompanyGetCompanyCLAGroupManagersHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": params.ClaGroupID, "companyID": params.CompanyID, @@ -94,7 +175,7 @@ func Configure(api *operations.EasyclaAPI, service Service, v1CompanyRepo v1Comp if err != nil { msg := "problem loading company CLA group managers" log.WithFields(f).WithError(err).Warn(msg) - if err == v1Company.ErrCompanyDoesNotExist { + if _, ok := err.(*utils.CompanyNotFound); ok { return company.NewGetCompanyCLAGroupManagersNotFound().WithXRequestID(reqID).WithPayload( utils.ErrorResponseNotFoundWithError(reqID, msg, err)) } @@ -111,49 +192,45 @@ func Configure(api *operations.EasyclaAPI, service Service, v1CompanyRepo v1Comp ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "CompanyGetCompanyProjectActiveClaHandler", + "functionName": "v2.company.handlers.CompanyGetCompanyProjectActiveClaHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": params.ProjectSFID, - "companySFID": params.CompanySFID, + "companyID": params.CompanyID, + "authUserName": utils.StringValue(params.XUSERNAME), + "authUserEmail": utils.StringValue(params.XEMAIL), } - log.WithFields(f).Debug("checking permissions") - if !utils.IsUserAuthorizedForOrganization(authUser, params.CompanySFID, utils.ALLOW_ADMIN_SCOPE) { - return company.NewGetCompanyProjectActiveClaForbidden().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "403", - Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to CreateCLAManager with Project|Organization scope of %s | %s", - authUser.UserName, params.ProjectSFID, params.CompanySFID), - XRequestID: reqID, - }) + // Lookup the company by internal ID + log.WithFields(f).Debugf("looking up company by internal ID...") + v2CompanyModel, err := service.GetCompanyByID(ctx, params.CompanyID) + if err != nil || v2CompanyModel == nil { + msg := fmt.Sprintf("unable to lookup company by ID: %s", params.CompanyID) + log.WithFields(f).WithError(err).Warn(msg) + return company.NewGetCompanyProjectActiveClaBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } - comp, err := v1CompanyRepo.GetCompanyByExternalID(ctx, params.CompanySFID) - if err != nil { - if err == v1Company.ErrCompanyDoesNotExist { - return company.NewGetCompanyProjectActiveClaNotFound().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "404", - Message: fmt.Sprintf("Company not found with given ID. [%s]", params.CompanySFID), - XRequestID: reqID, - }) - } - } - if comp == nil { - log.WithFields(f).WithError(err).Warn(msgUnableToLoadCompany) - return company.NewGetCompanyProjectActiveClaNotFound().WithXRequestID(reqID).WithPayload( - utils.ErrorResponseNotFound(reqID, msgUnableToLoadCompany)) + log.WithFields(f).Debug("checking permissions") + if !utils.IsUserAuthorizedForOrganization(ctx, authUser, v2CompanyModel.CompanyExternalID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to GetCompanyProjectActiveCla with Project|Organization scope of %s | %s", + authUser.UserName, params.ProjectSFID, v2CompanyModel.CompanyExternalID) + log.WithFields(f).Warn(msg) + return company.NewGetCompanyProjectActiveClaForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } - result, err := service.GetCompanyProjectActiveCLAs(ctx, comp.CompanyID, params.ProjectSFID) + log.WithFields(f).Debug("getting company project active CLAs...") + result, err := service.GetCompanyProjectActiveCLAs(ctx, v2CompanyModel.CompanyID, params.ProjectSFID) if err != nil { if strings.ContainsAny(err.Error(), "getProjectNotFound") { - return company.NewGetCompanyProjectActiveClaNotFound().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "404", - Message: fmt.Sprintf("clagroup not found with given ID. [%s]", params.ProjectSFID), - XRequestID: reqID, - }) + msg := fmt.Sprintf("CLA Group not found with given project SFID: %s", params.ProjectSFID) + log.WithFields(f).Warn(msg) + return company.NewGetCompanyProjectActiveClaNotFound().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbiddenWithError(reqID, msg, err)) } - return company.NewGetCompanyProjectActiveClaBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) + + msg := fmt.Sprintf("error looking up active project CLAs by internal company ID: %s and project SFID: %s", v2CompanyModel.CompanyID, params.ProjectSFID) + log.WithFields(f).Warn(msg) + return company.NewGetCompanyProjectActiveClaBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } + return company.NewGetCompanyProjectActiveClaOK().WithXRequestID(reqID).WithPayload(result) }) @@ -163,28 +240,61 @@ func Configure(api *operations.EasyclaAPI, service Service, v1CompanyRepo v1Comp ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "CompanyGetCompanyProjectContributorsHandler", + "functionName": "v2.company.handlers.CompanyGetCompanyProjectContributorsHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": params.ProjectSFID, - "companySFID": params.CompanySFID, + "companyID": params.CompanyID, + "searchTerm": utils.StringValue(params.SearchTerm), + "authUserName": utils.StringValue(params.XUSERNAME), + "authUserEmail": utils.StringValue(params.XEMAIL), } + // Lookup the company by internal ID + log.WithFields(f).Debugf("looking up company by internal ID...") + v1CompanyModel, err := service.GetCompanyByID(ctx, params.CompanyID) + if err != nil || v1CompanyModel == nil { + msg := fmt.Sprintf("unable to lookup company by ID: %s", params.CompanyID) + log.WithFields(f).WithError(err).Warn(msg) + if _, ok := err.(*utils.CompanyNotFound); ok { + return company.NewGetCompanyProjectActiveClaNotFound().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFoundWithError(reqID, msg, err)) + } + return company.NewGetCompanyProjectActiveClaBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + log.WithFields(f).Debugf("looked company by internal ID") + // PM - check if authorized by project scope - allow if PM has project ID scope that matches // Contact,Community Program Manager,CLA Manager,CLA Manager Designee,Company Admin - check if authorized by organization scope - allow if {Contact,Community Program Manager,CLA Manager,CLA Manager Designee,Company Admin} has organization ID scope that matches // CLA Manager - check if authorized by project|organization scope - allow if CLA Manager (for example) has project ID + org DI scope that matches log.WithFields(f).Debug("checking permissions") - if !isUserHaveAccessToCLAProjectOrganization(ctx, authUser, params.ProjectSFID, params.CompanySFID, projectClaGroupRepo) { + if !isUserHaveAccessToCLAProjectOrganization(ctx, authUser, params.ProjectSFID, v1CompanyModel.CompanyExternalID, projectClaGroupRepo) { + msg := fmt.Sprintf("user %s does not have access to get contributors with Project scope of %s or Project|Organization scope of %s | %s", + authUser.UserName, params.ProjectSFID, params.ProjectSFID, params.CompanyID) + log.WithFields(f).Warn(msg) return company.NewGetCompanyProjectContributorsForbidden().WithXRequestID(reqID).WithPayload( - utils.ErrorResponseForbidden( - reqID, - fmt.Sprintf("user %s does not have access to get contributors with Project scope of %s or Project|Organization scope of %s | %s", - authUser.UserName, params.ProjectSFID, params.ProjectSFID, params.CompanySFID))) + utils.ErrorResponseForbidden(reqID, msg)) } - result, err := service.GetCompanyProjectContributors(ctx, params.ProjectSFID, params.CompanySFID, utils.StringValue(params.SearchTerm)) + log.WithFields(f).Debugf("querying for employee contributors...") + //result, err := service.GetCompanyProjectContributors(ctx, params.ProjectSFID, params.CompanyID, utils.StringValue(params.SearchTerm)) + result, err := service.GetCompanyProjectContributors(ctx, ¶ms) if err != nil { - if err == v1Company.ErrCompanyDoesNotExist { - return company.NewGetCompanyProjectContributorsNotFound().WithXRequestID(reqID) + if companyErr, ok := err.(*utils.CompanyNotFound); ok { + msg := fmt.Sprintf("Company not found with ID: %s", companyErr.CompanyID) + log.WithFields(f).Warn(msg) + return company.NewGetCompanyProjectContributorsNotFound().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseNotFoundWithError(reqID, msg, err)) + } + if claGroupErr, ok := err.(*utils.CLAGroupNotFound); ok { + msg := fmt.Sprintf("CLA Group not found with ID: %s", claGroupErr.CLAGroupID) + log.WithFields(f).Warn(msg) + return company.NewGetCompanyProjectContributorsNotFound().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseNotFoundWithError(reqID, msg, err)) + } + if _, ok := err.(*utils.ProjectCLAGroupMappingNotFound); ok { + msg := fmt.Sprintf("CLA Group not found with project SFID: %s", params.ProjectSFID) + log.WithFields(f).Warn(msg) + return company.NewGetCompanyProjectContributorsNotFound().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseNotFoundWithError(reqID, msg, err)) } return company.NewGetCompanyProjectContributorsBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) } @@ -197,10 +307,12 @@ func Configure(api *operations.EasyclaAPI, service Service, v1CompanyRepo v1Comp ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "CompanyGetCompanyProjectClaHandler", + "functionName": "v2.company.handlers.CompanyGetCompanyProjectClaHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": params.ProjectSFID, "companySFID": params.CompanySFID, + "authUserName": utils.StringValue(params.XUSERNAME), + "authUserEmail": utils.StringValue(params.XEMAIL), } log.WithFields(f).Debug("checking permissions") @@ -211,11 +323,11 @@ func Configure(api *operations.EasyclaAPI, service Service, v1CompanyRepo v1Comp } log.WithFields(f).Debug("loading project company CLAs") - result, err := service.GetCompanyProjectCLA(ctx, authUser, params.CompanySFID, params.ProjectSFID) + result, err := service.GetCompanyProjectCLA(ctx, authUser, params.CompanySFID, params.ProjectSFID, params.CompanyID) if err != nil { msg := "unable to load project company CLAs" log.WithFields(f).WithError(err).Warn(msg) - if err == v1Company.ErrCompanyDoesNotExist { + if _, ok := err.(*utils.CompanyNotFound); ok { return company.NewGetCompanyProjectClaNotFound().WithXRequestID(reqID).WithPayload( utils.ErrorResponseNotFoundWithError(reqID, msg, err)) } @@ -235,12 +347,13 @@ func Configure(api *operations.EasyclaAPI, service Service, v1CompanyRepo v1Comp reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint f := logrus.Fields{ - "functionName": "CompanyCreateCompanyHandler", + "functionName": "v2.company.handlers.CompanyCreateCompanyHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "userID": params.UserID, "companyName": aws.StringValue(params.Input.CompanyName), "companyWebsite": aws.StringValue(params.Input.CompanyWebsite), - "signingEntityName": aws.StringValue(params.Input.SigningEntityName), + "signingEntityName": params.Input.SigningEntityName, + "userEmail": params.Input.UserEmail.String(), } // No permissions needed - anyone can create a company @@ -253,7 +366,7 @@ func Configure(api *operations.EasyclaAPI, service Service, v1CompanyRepo v1Comp } log.WithFields(f).Debug("creating company...") - companyModel, err := service.CreateCompany(ctx, *params.Input.CompanyName, *params.Input.SigningEntityName, *params.Input.CompanyWebsite, params.Input.UserEmail.String(), params.UserID) + companyModel, err := service.CreateCompany(ctx, ¶ms) if err != nil { log.Warnf("error returned from create company api: %+v", err) if strings.Contains(err.Error(), "website already exists") { @@ -274,22 +387,53 @@ func Configure(api *operations.EasyclaAPI, service Service, v1CompanyRepo v1Comp reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint f := logrus.Fields{ - "functionName": "CompanyGetCompanyByNameHandler", + "functionName": "v2.company.handlers.CompanyGetCompanyByNameHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "companyName": params.CompanyName, } - // Anyone can query for a company by name - log.WithFields(f).Debug("loading company by name") + // Anyone can query for a company by name - no permissions checks + + // Weird - sometimes the UI calls us with the company name of "null" + if params.CompanyName == "" || params.CompanyName == "null" { + return company.NewGetCompanyByNameBadRequest(). + WithXRequestID(reqID). + WithPayload(utils.ErrorResponseBadRequest(reqID, "company name input parameter missing or valid")) + } + + log.WithFields(f).Debugf("loading company by name: '%s'", params.CompanyName) companyModel, err := service.GetCompanyByName(ctx, params.CompanyName) - if err != nil { - msg := fmt.Sprintf("unable to locate company by name: %s", params.CompanyName) - log.WithFields(f).WithError(err).Warn(msg) - return company.NewGetCompanyByNameBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + if err != nil || companyModel == nil { + log.WithFields(f).Warnf("unable to lookup company by name '%s' in local database. trying organization service...", params.CompanyName) + osClient := organization_service.GetClient() + orgModels, orgLookupErr := osClient.SearchOrganization(ctx, params.CompanyName, "", "") + if orgLookupErr != nil || len(orgModels) == 0 { + msg := fmt.Sprintf("unable to locate organization '%s' in the organization service", params.CompanyName) + log.WithFields(f).WithError(err).Warn(msg) + return company.NewGetCompanyByNameNotFound().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFound(reqID, msg)) + } + + log.WithFields(f).Debugf("found company: '%s' in the organization service - creating local record...", params.CompanyName) + companyModelOutput, companyCreateErr := service.CreateCompanyFromSFModel(ctx, orgModels[0], authUser) + if companyCreateErr != nil || companyModelOutput == nil { + msg := fmt.Sprintf("unable to create company '%s' from salesforce record", params.CompanyName) + log.WithFields(f).WithError(err).Warn(msg) + return company.NewGetCompanyByNameInternalServerError().WithXRequestID(reqID).WithPayload(utils.ErrorResponseInternalServerErrorWithError(reqID, msg, companyCreateErr)) + } + + // Note: company name may have been swapped with actual value from SF or Clearbit authority - so use it below... + + log.WithFields(f).Debugf("loading company: %s by name after creation...", companyModelOutput.CompanyName) + companyModel, err = service.GetCompanyByName(ctx, companyModelOutput.CompanyName) + if err != nil { + msg := fmt.Sprintf("unable to locate company '%s' after creating...", companyModelOutput.CompanyName) + log.WithFields(f).WithError(err).Warn(msg) + return company.NewGetCompanyByNameNotFound().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFound(reqID, msg)) + } } if companyModel == nil { - msg := fmt.Sprintf("unable to locate company by name: %s", params.CompanyName) + msg := fmt.Sprintf("unable to load company by name: %s", params.CompanyName) log.WithFields(f).Warn(msg) return company.NewGetCompanyByNameNotFound().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFound(reqID, msg)) } @@ -297,15 +441,47 @@ func Configure(api *operations.EasyclaAPI, service Service, v1CompanyRepo v1Comp return company.NewGetCompanyByNameOK().WithXRequestID(reqID).WithPayload(companyModel) }) + api.CompanyGetCompanyBySigningEntityNameHandler = company.GetCompanyBySigningEntityNameHandlerFunc( + func(params company.GetCompanyBySigningEntityNameParams, authUser *auth.User) middleware.Responder { + reqID := utils.GetRequestID(params.XREQUESTID) + ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + f := logrus.Fields{ + "functionName": "v2.company.handlers.CompanyGetCompanyByNameHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "signingEntityName": params.SigningEntityName, + } + + // Anyone can query for a company by signing entity name + + log.WithFields(f).Debug("loading company by name") + companyModel, err := service.GetCompanyBySigningEntityName(ctx, params.SigningEntityName) + if err != nil { + msg := fmt.Sprintf("unable to locate company by signing entity name: %s", params.SigningEntityName) + log.WithFields(f).WithError(err).Warn(msg) + return company.NewGetCompanyBySigningEntityNameBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + + if companyModel == nil { + msg := fmt.Sprintf("unable to locate company by signing entity name: %s", params.SigningEntityName) + log.WithFields(f).Warn(msg) + return company.NewGetCompanyBySigningEntityNameNotFound().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFound(reqID, msg)) + } + + return company.NewGetCompanyBySigningEntityNameOK().WithXRequestID(reqID).WithPayload(companyModel) + }) + api.CompanyDeleteCompanyByIDHandler = company.DeleteCompanyByIDHandlerFunc( func(params company.DeleteCompanyByIDParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "CompanyDeleteCompanyByIDHandler", + "functionName": "v2.company.handlers.CompanyDeleteCompanyByIDHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "companyID": params.CompanyID, + "authUserName": utils.StringValue(params.XUSERNAME), + "authUserEmail": utils.StringValue(params.XEMAIL), } // Attempt to locate the company by ID @@ -333,7 +509,7 @@ func Configure(api *operations.EasyclaAPI, service Service, v1CompanyRepo v1Comp } // finally, we can check permissions for the delete operation - if !utils.IsUserAuthorizedForOrganization(authUser, companyModel.CompanyExternalID, utils.ALLOW_ADMIN_SCOPE) { + if !utils.IsUserAuthorizedForOrganization(ctx, authUser, companyModel.CompanyExternalID, utils.ALLOW_ADMIN_SCOPE) { msg := fmt.Sprintf(" user %s does not have access to company %s with Organization scope of %s", authUser.UserName, companyModel.CompanyName, companyModel.CompanyExternalID) log.Warn(msg) @@ -355,9 +531,11 @@ func Configure(api *operations.EasyclaAPI, service Service, v1CompanyRepo v1Comp ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "CompanyDeleteCompanyBySFIDHandler", + "functionName": "v2.company.handlers.CompanyDeleteCompanyBySFIDHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "companySFID": params.CompanySFID, + "authUserName": utils.StringValue(params.XUSERNAME), + "authUserEmail": utils.StringValue(params.XEMAIL), } // Attempt to locate the company by external SFID @@ -388,7 +566,7 @@ func Configure(api *operations.EasyclaAPI, service Service, v1CompanyRepo v1Comp // finally, we can check permissions for the delete operation log.WithFields(f).Debug("checking permissions") - if !utils.IsUserAuthorizedForOrganization(authUser, companyModel.CompanyExternalID, utils.ALLOW_ADMIN_SCOPE) { + if !utils.IsUserAuthorizedForOrganization(ctx, authUser, companyModel.CompanyExternalID, utils.ALLOW_ADMIN_SCOPE) { msg := fmt.Sprintf(" user %s does not have access to company %s with Organization scope of %s", authUser.UserName, companyModel.CompanyName, companyModel.CompanyExternalID) log.WithFields(f).Warn(msg) @@ -410,7 +588,7 @@ func Configure(api *operations.EasyclaAPI, service Service, v1CompanyRepo v1Comp reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint f := logrus.Fields{ - "functionName": "CompanyContributorAssociationHandler", + "functionName": "v2.company.handlers.CompanyContributorAssociationHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "companySFID": params.CompanySFID, "userEmail": params.Body.UserEmail.String(), @@ -436,7 +614,7 @@ func Configure(api *operations.EasyclaAPI, service Service, v1CompanyRepo v1Comp reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint f := logrus.Fields{ - "functionName": "CompanyContributorAssociationHandler", + "functionName": "v2.company.handlers.CompanyContributorAssociationHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "companySFID": params.CompanySFID, } @@ -492,9 +670,15 @@ func Configure(api *operations.EasyclaAPI, service Service, v1CompanyRepo v1Comp api.CompanySearchCompanyLookupHandler = company.SearchCompanyLookupHandlerFunc(func(params company.SearchCompanyLookupParams) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + f := logrus.Fields{ + "functionName": "v2.company.handlers.CompanyGetCompanyByInternalIDHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companyName": params.CompanyName, + "websiteName": params.WebsiteName, + } if params.CompanyName == nil && params.WebsiteName == nil { - log.Debugf("CompanyName or WebsiteName atleast one required") + log.WithFields(f).Debugf("CompanyName or WebsiteName at least one required") return company.NewSearchCompanyLookupBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(reqID, errors.New("companyName or websiteName at least one required"))) } @@ -503,7 +687,7 @@ func Configure(api *operations.EasyclaAPI, service Service, v1CompanyRepo v1Comp result, err := service.GetCompanyLookup(ctx, companyName, websiteName) if err != nil { msg := fmt.Sprintf("error occured while search orgname %s, websitename %s", companyName, websiteName) - log.Warnf("error occured while search orgname %s, websitename %s. error = %s", companyName, websiteName, err.Error()) + log.WithFields(f).WithError(err).Warnf("error occured while search orgname %s, websitename %s. error = %s", companyName, websiteName, err.Error()) if _, ok := err.(*organizations.LookupNotFound); ok { return company.NewSearchCompanyLookupNotFound().WithXRequestID(reqID).WithPayload( utils.ErrorResponseNotFoundWithError(reqID, msg, err)) @@ -536,7 +720,7 @@ func errorResponse(reqID string, err error) *models.ErrorResponse { // isUserHaveAccessToCLAProjectOrganization is a helper function to determine if the user has access to the specified project and organization func isUserHaveAccessToCLAProjectOrganization(ctx context.Context, authUser *auth.User, projectSFID, organizationSFID string, projectClaGroupsRepo projects_cla_groups.Repository) bool { f := logrus.Fields{ - "functionName": "isUserHaveAccessToCLAProjectOrganization", + "functionName": "v2.company.handlers.isUserHaveAccessToCLAProjectOrganization", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": projectSFID, "organizationSFID": organizationSFID, @@ -545,31 +729,31 @@ func isUserHaveAccessToCLAProjectOrganization(ctx context.Context, authUser *aut } log.WithFields(f).Debug("testing if user has access to project SFID...") - if utils.IsUserAuthorizedForProject(authUser, projectSFID, utils.ALLOW_ADMIN_SCOPE) { + if utils.IsUserAuthorizedForProject(ctx, authUser, projectSFID, utils.ALLOW_ADMIN_SCOPE) { log.WithFields(f).Debug("user has access to project SFID...") return true } log.WithFields(f).Debug("testing if user has access to project SFID tree...") - if utils.IsUserAuthorizedForProjectTree(authUser, projectSFID, utils.ALLOW_ADMIN_SCOPE) { + if utils.IsUserAuthorizedForProjectTree(ctx, authUser, projectSFID, utils.ALLOW_ADMIN_SCOPE) { log.WithFields(f).Debug("user has access to project SFID tree...") return true } log.WithFields(f).Debug("testing if user has access to project SFID and organization SFID...") - if utils.IsUserAuthorizedForProjectOrganization(authUser, projectSFID, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { + if utils.IsUserAuthorizedForProjectOrganization(ctx, authUser, projectSFID, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { log.WithFields(f).Debug("user has access to project SFID and organization SFID...") return true } log.WithFields(f).Debug("testing if user has access to project SFID and organization SFID tree...") - if utils.IsUserAuthorizedForProjectOrganizationTree(authUser, projectSFID, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { + if utils.IsUserAuthorizedForProjectOrganizationTree(ctx, authUser, projectSFID, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { log.WithFields(f).Debug("user has access to project SFID and organization SFID tree...") return true } log.WithFields(f).Debug("testing if user has access to organization SFID...") - if utils.IsUserAuthorizedForOrganization(authUser, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { + if utils.IsUserAuthorizedForOrganization(ctx, authUser, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { log.WithFields(f).Debug("user has access to organization SFID...") return true } @@ -578,7 +762,7 @@ func isUserHaveAccessToCLAProjectOrganization(ctx context.Context, authUser *aut // other projects or the parent project group/foundation log.WithFields(f).Debug("user doesn't have direct access to the project only, project + organization, or organization only - loading CLA Group from project id...") - projectCLAGroupModel, err := projectClaGroupsRepo.GetClaGroupIDForProject(projectSFID) + projectCLAGroupModel, err := projectClaGroupsRepo.GetClaGroupIDForProject(ctx, projectSFID) if err != nil { log.WithFields(f).WithError(err).Warnf("problem loading project -> cla group mapping - returning false") return false @@ -591,31 +775,31 @@ func isUserHaveAccessToCLAProjectOrganization(ctx context.Context, authUser *aut // Check the foundation permissions f["foundationSFID"] = projectCLAGroupModel.FoundationSFID log.WithFields(f).Debug("testing if user has access to parent foundation...") - if utils.IsUserAuthorizedForProject(authUser, projectCLAGroupModel.FoundationSFID, utils.ALLOW_ADMIN_SCOPE) { + if utils.IsUserAuthorizedForProject(ctx, authUser, projectCLAGroupModel.FoundationSFID, utils.ALLOW_ADMIN_SCOPE) { log.WithFields(f).Debug("user has access to parent foundation...") return true } log.WithFields(f).Debug("testing if user has access to parent foundation truee...") - if utils.IsUserAuthorizedForProjectTree(authUser, projectCLAGroupModel.FoundationSFID, utils.ALLOW_ADMIN_SCOPE) { + if utils.IsUserAuthorizedForProjectTree(ctx, authUser, projectCLAGroupModel.FoundationSFID, utils.ALLOW_ADMIN_SCOPE) { log.WithFields(f).Debug("user has access to parent foundation tree...") return true } log.WithFields(f).Debug("testing if user has access to foundation SFID and organization SFID...") - if utils.IsUserAuthorizedForProjectOrganization(authUser, projectCLAGroupModel.FoundationSFID, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { + if utils.IsUserAuthorizedForProjectOrganization(ctx, authUser, projectCLAGroupModel.FoundationSFID, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { log.WithFields(f).Debug("user has access to foundation SFID and organization SFID...") return true } log.WithFields(f).Debug("testing if user has access to foundation SFID and organization SFID tree...") - if utils.IsUserAuthorizedForProjectOrganizationTree(authUser, projectCLAGroupModel.FoundationSFID, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { + if utils.IsUserAuthorizedForProjectOrganizationTree(ctx, authUser, projectCLAGroupModel.FoundationSFID, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { log.WithFields(f).Debug("user has access to foundation SFID and organization SFID tree...") return true } // Lookup the other project IDs associated with this CLA Group log.WithFields(f).Debug("looking up other projects associated with the CLA Group...") - projectCLAGroupModels, err := projectClaGroupsRepo.GetProjectsIdsForClaGroup(projectCLAGroupModel.ClaGroupID) + projectCLAGroupModels, err := projectClaGroupsRepo.GetProjectsIdsForClaGroup(ctx, projectCLAGroupModel.ClaGroupID) if err != nil { log.WithFields(f).WithError(err).Warnf("problem loading project cla group mappings by CLA Group ID - returning false") return false @@ -624,7 +808,7 @@ func isUserHaveAccessToCLAProjectOrganization(ctx context.Context, authUser *aut projectSFIDs := getProjectIDsFromModels(f, projectCLAGroupModel.FoundationSFID, projectCLAGroupModels) f["projectIDs"] = strings.Join(projectSFIDs, ",") log.WithFields(f).Debug("testing if user has access to any cla group project + organization") - if utils.IsUserAuthorizedForAnyProjectOrganization(authUser, projectSFIDs, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { + if utils.IsUserAuthorizedForAnyProjectOrganization(ctx, authUser, projectSFIDs, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { log.WithFields(f).Debug("user has access to at least of of the projects...") return true } diff --git a/cla-backend-go/v2/company/service.go b/cla-backend-go/v2/company/service.go index 62776c054..373daaa36 100644 --- a/cla-backend-go/v2/company/service.go +++ b/cla-backend-go/v2/company/service.go @@ -12,12 +12,13 @@ import ( "sync" "time" + "github.com/communitybridge/easycla/cla-backend-go/project/repository" + "github.com/go-openapi/strfmt" "github.com/sirupsen/logrus" "github.com/communitybridge/easycla/cla-backend-go/events" - "github.com/communitybridge/easycla/cla-backend-go/project" "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" "github.com/jinzhu/copier" @@ -27,17 +28,19 @@ import ( "github.com/aws/aws-sdk-go/aws" v1Company "github.com/communitybridge/easycla/cla-backend-go/company" - v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/models" - v1ProjectParams "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/project" - v1SignatureParams "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/signatures" + v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + v1ProjectParams "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/project" + v1SignatureParams "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/signatures" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" v2Models "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/signatures" "github.com/communitybridge/easycla/cla-backend-go/users" "github.com/communitybridge/easycla/cla-backend-go/utils" - acs_service "github.com/communitybridge/easycla/cla-backend-go/v2/acs-service" + acsService "github.com/communitybridge/easycla/cla-backend-go/v2/acs-service" + orgModels "github.com/communitybridge/easycla/cla-backend-go/v2/organization-service/models" + v2Ops "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations/company" orgService "github.com/communitybridge/easycla/cla-backend-go/v2/organization-service" "github.com/communitybridge/easycla/cla-backend-go/v2/organization-service/client/organizations" v2ProjectService "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" @@ -69,28 +72,28 @@ var ( // constants const ( - // used when we want to query all data from dependent service. + // HugePageSize is used when we want to query all data from dependent service HugePageSize = int64(10000) - // LoadRepoDetails = true + DontLoadRepoDetails = false - // FoundationType the SF foundation type string - previously was "Foundation", now "Project Group" - FoundationType = "Project Group" - // Lead representing type of user - Lead = "lead" - //NoAccount + + // NoAccount constant NoAccount = "Individual - No Account" + //OrgAssociated stating whether user has user association with another org OrgAssociated = "are already associated with other organization" ) // Service functions for company type Service interface { - GetCompanyProjectCLAManagers(ctx context.Context, companyID, companySFID, projectSFID string) (*models.CompanyClaManagers, error) + GetCompanyProjectCLAManagers(ctx context.Context, v1CompanyModel *models.Company, projectSFID string) (*models.CompanyClaManagers, error) GetCompanyProjectActiveCLAs(ctx context.Context, companyID string, projectSFID string) (*models.ActiveClaList, error) - GetCompanyProjectContributors(ctx context.Context, projectSFID string, companySFID string, searchTerm string) (*models.CorporateContributorList, error) - GetCompanyProjectCLA(ctx context.Context, authUser *auth.User, companySFID, projectSFID string) (*models.CompanyProjectClaList, error) - CreateCompany(ctx context.Context, companyName, signingEntityName, companyWebsite, userEmail, userID string) (*models.CompanyOutput, error) + GetCompanyProjectContributors(ctx context.Context, params *v2Ops.GetCompanyProjectContributorsParams) (*models.CorporateContributorList, error) + GetCompanyProjectCLA(ctx context.Context, authUser *auth.User, companySFID, projectSFID string, companyID *string) (*models.CompanyProjectClaList, error) + CreateCompany(ctx context.Context, params *v2Ops.CreateCompanyParams) (*models.CompanyOutput, error) + CreateCompanyFromSFModel(ctx context.Context, orgModel *orgModels.Organization, authUser *auth.User) (*models.CompanyOutput, error) GetCompanyByName(ctx context.Context, companyName string) (*models.Company, error) + GetCompanyBySigningEntityName(ctx context.Context, signingEntityName string) (*models.Company, error) GetCompanyByID(ctx context.Context, companyID string) (*models.Company, error) GetCompanyBySFID(ctx context.Context, companySFID string) (*models.Company, error) DeleteCompanyByID(ctx context.Context, companyID string) error @@ -101,7 +104,7 @@ type Service interface { GetCompanyAdmins(ctx context.Context, companyID string) (*models.CompanyAdminList, error) RequestCompanyAdmin(ctx context.Context, userID string, claManagerEmail string, claManagerName string, contributorName string, contributorEmail string, projectName string, companyName string, lFxPortalURL string) error - // org service lookup + // GetCompanyLookup uses the org service to lookup the value GetCompanyLookup(ctx context.Context, companyName string, websiteName string) (*models.Lookup, error) } @@ -125,13 +128,18 @@ func NewService(v1CompanyService v1Company.IService, sigRepo signatures.Signatur } } -func (s *service) GetCompanyProjectCLAManagers(ctx context.Context, companyID, companySFID, projectSFID string) (*models.CompanyClaManagers, error) { +func (s *service) GetCompanyProjectCLAManagers(ctx context.Context, v1CompanyModel *models.Company, projectSFID string) (*models.CompanyClaManagers, error) { f := logrus.Fields{ - "functionName": "GetCompanyProjectCLAManagers", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "projectSFID": projectSFID, - "companyID": companyID, + "functionName": "v2.company.service.GetCompanyProjectCLAManagers", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + "companyID": v1CompanyModel.CompanyID, + "companySFID": v1CompanyModel.CompanyExternalID, + "companyName": v1CompanyModel.CompanyName, + "signingEntityName": v1CompanyModel.SigningEntityName, } + + // TODO: DAD - consider using separate go routine log.WithFields(f).Debugf("locating CLA Group(s) under project or foundation...") var err error claGroups, err := s.getCLAGroupsUnderProjectOrFoundation(ctx, projectSFID) @@ -140,13 +148,15 @@ func (s *service) GetCompanyProjectCLAManagers(ctx context.Context, companyID, c return nil, err } + // TODO: DAD - consider using separate go routine // get the org client for org info filling orgClient := orgService.GetClient() - orgModel, err := orgClient.GetOrganization(ctx, companySFID) + orgModel, err := orgClient.GetOrganization(ctx, v1CompanyModel.CompanyExternalID) if err != nil { - return nil, fmt.Errorf("fetching org model failed for companySFID : %s : %w", companySFID, err) + return nil, fmt.Errorf("fetching org model failed for companySFID : %s : %w", v1CompanyModel.CompanyExternalID, err) } + // TODO: DAD - consider using separate go routine signed, approved := true, true maxLoad := int64(10) var sigs []*v1Models.Signature @@ -157,7 +167,7 @@ func (s *service) GetCompanyProjectCLAManagers(ctx context.Context, companyID, c log.WithFields(f).Debugf("claGroupID missing for project : %s ", claGroup.ProjectSFID) continue } - sig, sigErr := s.signatureRepo.GetProjectCompanySignature(ctx, companyID, claGroup.ClaGroupID, &signed, &approved, nil, &maxLoad) + sig, sigErr := s.signatureRepo.GetProjectCompanySignature(ctx, v1CompanyModel.CompanyID, claGroup.ClaGroupID, &signed, &approved, nil, &maxLoad) if sigErr != nil { log.WithFields(f).Warnf("problem fetching CLA signatures, error: %+v", sigErr) return nil, sigErr @@ -178,11 +188,13 @@ func (s *service) GetCompanyProjectCLAManagers(ctx context.Context, companyID, c for _, user := range sig.SignatureACL { claManagers = append(claManagers, &models.CompanyClaManager{ // DB doesn't have approved_on value - ApprovedOn: sig.SignatureCreated, - LfUsername: user.LfUsername, - ProjectID: sig.ProjectID, - OrganizationSfid: companySFID, - OrganizationName: orgModel.Name, + ApprovedOn: sig.SignatureCreated, + LfUsername: user.LfUsername, + ProjectID: sig.ProjectID, + OrganizationSfid: v1CompanyModel.CompanyExternalID, + OrganizationID: v1CompanyModel.CompanyID, + OrganizationName: orgModel.Name, + SigningEntityName: v1CompanyModel.SigningEntityName, }) lfUsernames.Add(user.LfUsername) } @@ -192,6 +204,7 @@ func (s *service) GetCompanyProjectCLAManagers(ctx context.Context, companyID, c return &models.CompanyClaManagers{List: claManagers}, nil } + // TODO: DAD - consider using separate go routine // get userinfo and project info var usermap map[string]*v2UserServiceModels.User usermap, err = getUsersInfo(lfUsernames.List()) @@ -204,10 +217,12 @@ func (s *service) GetCompanyProjectCLAManagers(ctx context.Context, companyID, c fillUsersInfo(claManagers, usermap) // fill project info fillProjectInfo(claManagers, claGroups) + + // TODO: DAD - consider using separate go routine // fetch the cla_manager.added events so can fill the addedOn field - claManagerAddedEvents, err := s.eventService.GetCompanyEvents(companyID, events.ClaManagerCreated, nil, aws.Int64(100), true) + claManagerAddedEvents, err := s.eventService.GetCompanyEvents(v1CompanyModel.CompanyID, events.ClaManagerCreated, nil, aws.Int64(100), true) if err != nil { - log.WithFields(f).Warnf("fetching events for companyID failed : %s : %v", companyID, err) + log.WithFields(f).Warnf("fetching events for companyID failed : %s : %v", v1CompanyModel.CompanyID, err) return nil, err } // fill events info @@ -242,7 +257,7 @@ func fillEventsInfo(claManagers []*v2Models.CompanyClaManager, addedEvents *v1Mo func (s *service) GetCompanyAdmins(ctx context.Context, companySFID string) (*models.CompanyAdminList, error) { f := logrus.Fields{ - "functionName": "GetCompanyAdmins", + "functionName": "v2.company.service.GetCompanyAdmins", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "companySFID": companySFID, } @@ -277,7 +292,7 @@ func (s *service) GetCompanyAdmins(ctx context.Context, companySFID string) (*mo func (s *service) GetCompanyProjectActiveCLAs(ctx context.Context, companyID string, projectSFID string) (*models.ActiveClaList, error) { f := logrus.Fields{ - "functionName": "GetCompanyProjectActiveCLAs", + "functionName": "v2.company.service.GetCompanyProjectActiveCLAs", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": projectSFID, "companyID": companyID, @@ -309,88 +324,131 @@ func (s *service) GetCompanyProjectActiveCLAs(ctx context.Context, companyID str activeCla := &models.ActiveCla{} out.List = append(out.List, activeCla) go func(swg *sync.WaitGroup, signature *v1Models.Signature, acla *models.ActiveCla) { - s.fillActiveCLA(swg, signature, acla, claGroups) + s.fillActiveCLA(ctx, swg, signature, acla, claGroups, companyID) }(&wg, sig, activeCla) } wg.Wait() return &out, nil } -func (s *service) GetCompanyProjectContributors(ctx context.Context, projectSFID string, companySFID string, searchTerm string) (*models.CorporateContributorList, error) { +// GetCompanyProjectContributors by the specified parameters which include the project SFID, company ID and any additional search terms with pagination details +func (s *service) GetCompanyProjectContributors(ctx context.Context, params *v2Ops.GetCompanyProjectContributorsParams) (*models.CorporateContributorList, error) { f := logrus.Fields{ - "functionName": "GetCompanyProjectContributors", + "functionName": "v2.company.service.GetCompanyProjectContributors", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "projectSFID": projectSFID, - "companySFID": companySFID, - "searchTerm": searchTerm, + "projectSFID": params.ProjectSFID, + "companyID": params.CompanyID, + } + + if params.SearchTerm != nil { + f["searchTerm"] = utils.StringValue(params.SearchTerm) } + if params.PageSize != nil { + f["pageSize"] = utils.Int64Value(params.PageSize) + } + if params.NextKey != nil { + f["nextKey"] = utils.StringValue(params.NextKey) + } + list := make([]*models.CorporateContributor, 0) - sigs, err := s.getAllCompanyProjectEmployeeSignatures(ctx, companySFID, projectSFID) + log.WithFields(f).Debugf("querying for employee contributors...") + sigResponse, err := s.getAllCompanyProjectEmployeeSignatures(ctx, params) if err != nil { - log.WithFields(f).Warnf("problem fetching all company project employee signatures, error: %+v", err) + log.WithFields(f).WithError(err).Warn("problem fetching all company project employee signatures") return nil, err } - if len(sigs) == 0 { + if len(sigResponse.Signatures) == 0 { + log.WithFields(f).Debug("not signatures found - returning emtpy list") return &models.CorporateContributorList{ List: list, }, nil } + log.WithFields(f).Debugf("found %d signatures matching filter critiera - total in database is: %d", len(sigResponse.Signatures), sigResponse.TotalCount) + + beforeQuery, _ := utils.CurrentTime() var wg sync.WaitGroup result := make(chan *models.CorporateContributor) - wg.Add(len(sigs)) + wg.Add(len(sigResponse.Signatures)) go func() { wg.Wait() + log.WithFields(f).Debugf("done additional corporate contributor details for %d signatures...duration: %+v", len(sigResponse.Signatures), time.Since(beforeQuery)) close(result) }() - for _, sig := range sigs { - go fillCorporateContributorModel(&wg, s.userRepo, sig, result, searchTerm) + log.WithFields(f).Debugf("adding additional corporate contributor details for %d signatures...", len(sigResponse.Signatures)) + for _, sig := range sigResponse.Signatures { + go fillCorporateContributorModel(&wg, s.userRepo, sig, result) } for corpContributor := range result { list = append(list, corpContributor) } + // sort the list based on timestamp in descending order + sort.Slice(list, func(i, j int) bool { + return list[i].Timestamp > list[j].Timestamp + }) + return &models.CorporateContributorList{ - List: list, + List: list, + NextKey: sigResponse.LastKeyScanned, + ResultCount: sigResponse.ResultCount, + TotalCount: sigResponse.TotalCount, }, nil } -func (s *service) CreateCompany(ctx context.Context, companyName, signingEntityName, companyWebsite, userEmail, userID string) (*models.CompanyOutput, error) { +func (s *service) CreateCompany(ctx context.Context, params *v2Ops.CreateCompanyParams) (*models.CompanyOutput, error) { f := logrus.Fields{ - "functionName": "CreateCompany", + "functionName": "v2.company.service.CreateCompany", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "companyName": companyName, - "signingEntityName": signingEntityName, - "companyWebsite": companyWebsite, - "userEmail": userEmail, - "userID": userID, + "companyName": params.Input.CompanyName, + "signingEntityName": params.Input.SigningEntityName, + "companyWebsite": params.Input.CompanyWebsite, + "userEmail": params.Input.UserEmail.String(), + "userID": params.UserID, + "note": params.Input.Note, } + var lfUser *v2UserServiceModels.User + companyName := *params.Input.CompanyName + signingEntityName := params.Input.SigningEntityName + companyWebsite := *params.Input.CompanyWebsite + userEmail := params.Input.UserEmail.String() + userID := params.UserID + note := params.Input.Note // Create SalesForce company orgClient := orgService.GetClient() - log.WithFields(f).Debugf("Creating Organization: %s, Signing Entity Name: %s, Website: %s", companyName, signingEntityName, companyWebsite) + log.WithFields(f).Debugf("Creating Organization: %s, Signing Entity Name: %s, Website: %s in SalesForce...", companyName, signingEntityName, companyWebsite) org, err := orgClient.CreateOrg(ctx, companyName, signingEntityName, companyWebsite) if err != nil { log.WithFields(f).Warnf("unable to create platform organization service, error: %+v", err) return nil, err } - acsClient := acs_service.GetClient() + // Company Service switched the company name based on ClearBit??? + if org.Name != companyName { + log.WithFields(f).Debugf("create SalesForce company changed the company name - new name is: %s", org.Name) + companyName = org.Name + signingEntityName = org.Name + f["updatedCompanyName"] = org.Name + f["updatedSigningEntityName"] = org.Name + } + + acsClient := acsService.GetClient() userClient := v2UserService.GetClient() - lfUser, lfErr := userClient.SearchUserByEmail(userEmail) + lfUser, lfErr := userClient.SearchUsersByEmail(userEmail) if lfErr != nil { msg := fmt.Sprintf("User : %s has no LFID", userEmail) log.WithFields(f).Warn(msg) } if lfUser != nil && lfUser.Username == "" { - msg := fmt.Sprintf("User: %s has no LF username", userEmail) + msg := fmt.Sprintf("User: %+v has no LF login/username", lfUser) log.WithFields(f).Warn(msg) } if lfUser != nil && lfUser.Username != "" { - log.WithFields(f).Debugf("User :%s has been assigned the %s role to organization: %s ", + log.WithFields(f).Debugf("User: %s has been assigned the %s role to organization: %s ", userEmail, utils.CompanyAdminRole, org.Name) // Assign company-admin to user roleID, adminErr := acsClient.GetRoleID(utils.CompanyAdminRole) @@ -416,19 +474,29 @@ func (s *service) CreateCompany(ctx context.Context, companyName, signingEntityN } // Create Easy CLA Company - log.WithFields(f).Debugf("Creating EasyCLA company: %s ", companyName) + log.WithFields(f).Debugf("Creating EasyCLA company: %s", companyName) + + if signingEntityName == "" { + log.WithFields(f).Debugf("Setting signing entity with company name value: %s", companyName) + signingEntityName = companyName + } + // OrgID used as externalID for the easyCLA Company // Create a new company model for the create function createCompanyModel := &v1Models.Company{ - CompanyACL: nil, CompanyExternalID: org.ID, CompanyManagerID: userID, CompanyName: companyName, SigningEntityName: signingEntityName, + Note: note, + } + if lfUser != nil && lfUser.Username != "" { + createCompanyModel.CompanyACL = []string{lfUser.Username} + } else { + createCompanyModel.CompanyACL = []string{} } _, createErr := s.companyRepo.CreateCompany(ctx, createCompanyModel) - //easyCLAErr := s.repo.CreateCompany(companyName, org.ID, userID) if createErr != nil { log.WithFields(f).Warnf("Failed to create EasyCLA company for company: %s, error: %+v", companyName, createErr) @@ -444,10 +512,44 @@ func (s *service) CreateCompany(ctx context.Context, companyName, signingEntityN }, nil } +func (s *service) CreateCompanyFromSFModel(ctx context.Context, orgModel *orgModels.Organization, authUser *auth.User) (*models.CompanyOutput, error) { + f := logrus.Fields{ + "functionName": "v2.company.service.CreateCompanyFromSFModel", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "organizationID": orgModel.Name, + "organizationName": orgModel.Name, + "organizationType": orgModel.Type, + "organizationLink": orgModel.Link, + "organizationStatus": orgModel.Status, + } + + log.WithFields(f).Debugf("Creating company: %s...", orgModel.Name) + companyInput := &models.CompanyInput{ + CompanyName: &orgModel.Name, + CompanyWebsite: &orgModel.Link, + Note: fmt.Sprintf("created from platform organization service model: %s", orgModel.ID), + SigningEntityName: orgModel.Name, + } + if orgModel.Owner != nil { + userServiceClient := v2UserService.GetClient() + userModel, userLookupErr := userServiceClient.GetUser(orgModel.ID) + if userLookupErr != nil { + log.WithFields(f).WithError(userLookupErr).Warnf("unable to lookup user by SFID: %s", orgModel.ID) + } else { + userEmail := strfmt.Email(*userModel.Email) + companyInput.UserEmail = &userEmail + } + } + return s.CreateCompany(ctx, &v2Ops.CreateCompanyParams{ + Input: companyInput, + UserID: authUser.UserName, + }) +} + // GetCompanyByName deletes the company by name func (s *service) GetCompanyByName(ctx context.Context, companyName string) (*models.Company, error) { f := logrus.Fields{ - "functionName": "GetCompanyByName", + "functionName": "v2.company.service.GetCompanyByName", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "companyName": companyName, } @@ -472,10 +574,62 @@ func (s *service) GetCompanyByName(ctx context.Context, companyName string) (*mo return &v2CompanyModel, nil } +// GetCompanyBySigningEntityName retrieves the company by signing entity name +func (s *service) GetCompanyBySigningEntityName(ctx context.Context, signingEntityName string) (*models.Company, error) { + f := logrus.Fields{ + "functionName": "v2.company.service.GetCompanyBySigningEntityName", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "signingEntityName": signingEntityName, + } + + log.WithFields(f).Warn("looking up company record by signing entity name...") + companyModel, err := s.companyRepo.GetCompanyBySigningEntityName(ctx, signingEntityName) + if err != nil { + if _, ok := err.(*utils.CompanyNotFound); ok { // nolint + // As a backup, in case the signing entity name was not set on the old records, lookup the company by it's normal name + log.WithFields(f).Debugf("signing entity name not found. as a backup, searching company by name using signing entity name value: %s", signingEntityName) + companyModel, err = s.companyRepo.GetCompanyByName(ctx, signingEntityName) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to lookup company name by attempting to use the signing entity name") + return nil, err + } + } else { + log.WithFields(f).WithError(err).Warn("unable to lookup company by signing entity name") + return nil, err + } + } + + if companyModel == nil { + log.WithFields(f).Debugf("search by company signing entity name: %s didn't locate the record", signingEntityName) + // As a backup, in case the signing entity name was not set on the old records, lookup the company by it's normal name + log.WithFields(f).Debugf("as a backup, searching company by name using signing entity name value: %s", signingEntityName) + companyModel, err = s.companyRepo.GetCompanyByName(ctx, signingEntityName) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to lookup company name by attempting to use the signing entity name") + return nil, err + } + + if companyModel == nil { + log.WithFields(f).Debugf("search by company name: %s didn't locate the record", signingEntityName) + return nil, nil + } + } + + // Convert from v1 to v2 model - use helper: Copy(toValue interface{}, fromValue interface{}) + var v2CompanyModel v2Models.Company + copyErr := copier.Copy(&v2CompanyModel, &companyModel) + if copyErr != nil { + log.WithFields(f).Warnf("problem converting v1 company model to a v2 company model, error: %+v", copyErr) + return nil, copyErr + } + + return &v2CompanyModel, nil +} + // GetCompanyByID retrieves the company by internal ID func (s *service) GetCompanyByID(ctx context.Context, companyID string) (*models.Company, error) { f := logrus.Fields{ - "functionName": "GetCompanyByID", + "functionName": "v2.company.service.GetCompanyByID", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "companyID": companyID, } @@ -502,7 +656,7 @@ func (s *service) GetCompanyByID(ctx context.Context, companyID string) (*models func (s *service) AssociateContributor(ctx context.Context, companySFID string, userEmail string) (*models.Contributor, error) { f := logrus.Fields{ - "functionName": "AssociateContributor", + "functionName": "v2.company.service.AssociateContributor", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "companySFID": companySFID, "userEmail": userEmail, @@ -512,13 +666,13 @@ func (s *service) AssociateContributor(ctx context.Context, companySFID string, userService := v2UserService.GetClient() log.WithFields(f).Info("searching for LFX User") - lfxUser, userErr := userService.SearchUserByEmail(userEmail) + lfxUser, userErr := userService.SearchUsersByEmail(userEmail) if userErr != nil { log.WithFields(f).Warnf("unable to get user") return nil, userErr } - acsServiceClient := acs_service.GetClient() + acsServiceClient := acsService.GetClient() log.WithFields(f).Info("Getting roleID for the contributor role") roleID, roleErr := acsServiceClient.GetRoleID("contributor") @@ -546,10 +700,10 @@ func (s *service) AssociateContributor(ctx context.Context, companySFID string, return contributor, nil } -//CreateContributor creates contributor for contributor prospect +// CreateContributor creates contributor for contributor prospect func (s *service) CreateContributor(ctx context.Context, companyID string, projectID string, userEmail string, ClaGroupID string) (*models.Contributor, error) { f := logrus.Fields{ - "functionName": "CreateContributor", + "functionName": "v2.company.service.CreateContributor", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "companyID": companyID, "projectID": projectID, @@ -558,10 +712,10 @@ func (s *service) CreateContributor(ctx context.Context, companyID string, proje } // integrate user,acs,org and project services userClient := v2UserService.GetClient() - acServiceClient := acs_service.GetClient() + acServiceClient := acsService.GetClient() orgClient := orgService.GetClient() - user, userErr := userClient.SearchUserByEmail(userEmail) + user, userErr := userClient.SearchUsersByEmail(userEmail) if userErr != nil { log.WithFields(f).Debugf("Failed to get user by email: %s , error: %+v", userEmail, userErr) return nil, ErrLFXUserNotFound @@ -610,18 +764,19 @@ func (s *service) CreateContributor(ctx context.Context, companyID string, proje } // Log Event - s.eventService.LogEvent( + s.eventService.LogEventWithContext(ctx, &events.LogEventArgs{ - EventType: events.AssignUserRoleScopeType, - LfUsername: user.Username, - UserID: user.ID, - ExternalProjectID: projectID, - CompanyModel: v1CompanyModel, - ClaGroupModel: projectModel, - UserModel: &v1Models.User{LfUsername: user.Username, UserID: user.ID}, + EventType: events.AssignUserRoleScopeType, + ProjectSFID: projectID, + CompanyModel: v1CompanyModel, + ClaGroupModel: projectModel, + CLAGroupID: projectModel.ProjectID, + CLAGroupName: projectModel.ProjectName, EventData: &events.AssignRoleScopeData{ - Role: "contributor", - Scope: fmt.Sprintf("%s|%s", projectID, companyID), + Role: "contributor", + Scope: fmt.Sprintf("%s|%s", projectID, companyID), + UserName: user.Username, + UserEmail: utils.StringValue(user.Email), }, }) @@ -636,10 +791,10 @@ func (s *service) CreateContributor(ctx context.Context, companyID string, proje return contributor, nil } -//AssociateContributorByGroup creates contributor by group for contributor prospect +// AssociateContributorByGroup creates contributor by group for contributor prospect func (s *service) AssociateContributorByGroup(ctx context.Context, companySFID, userEmail string, projectCLAGroups []*projects_cla_groups.ProjectClaGroup, ClaGroupID string) ([]*models.Contributor, string, error) { f := logrus.Fields{ - "functionName": "AssociateContributorByGroup", + "functionName": "v2.company.service.AssociateContributorByGroup", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "companySFID": companySFID, "ClaGroupID": ClaGroupID, @@ -682,7 +837,7 @@ func (s *service) AssociateContributorByGroup(ctx context.Context, companySFID, // GetCompanyBySFID retrieves the company by external SFID func (s *service) GetCompanyBySFID(ctx context.Context, companySFID string) (*models.Company, error) { f := logrus.Fields{ - "functionName": "GetCompanyBySFID", + "functionName": "v2.company.service.GetCompanyBySFID", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "companySFID": companySFID, } @@ -690,7 +845,7 @@ func (s *service) GetCompanyBySFID(ctx context.Context, companySFID string) (*mo if err != nil { // If we were unable to find the company/org in our local database, try to auto-create based // on the existing SF record - if err == company.ErrCompanyDoesNotExist { + if _, ok := err.(*utils.CompanyNotFound); ok { log.WithFields(f).Debug("company not found in EasyCLA database - attempting to auto-create from platform organization service record") newCompanyModel, createCompanyErr := s.autoCreateCompany(ctx, companySFID) if createCompanyErr != nil { @@ -700,7 +855,10 @@ func (s *service) GetCompanyBySFID(ctx context.Context, companySFID string) (*mo } if newCompanyModel == nil { log.WithFields(f).Warnf("problem creating company from SF records - created model is nil") - return nil, company.ErrCompanyDoesNotExist + return nil, &utils.CompanyNotFound{ + Message: "unable to auto-create company", + CompanySFID: companySFID, + } } // Success, fall through and continue processing companyModel = newCompanyModel @@ -734,15 +892,16 @@ func (s *service) DeleteCompanyBySFID(ctx context.Context, companyID string) err return s.companyRepo.DeleteCompanyBySFID(ctx, companyID) } -func (s *service) GetCompanyProjectCLA(ctx context.Context, authUser *auth.User, companySFID, projectSFID string) (*models.CompanyProjectClaList, error) { +func (s *service) GetCompanyProjectCLA(ctx context.Context, authUser *auth.User, companySFID, projectSFID string, companyID *string) (*models.CompanyProjectClaList, error) { f := logrus.Fields{ - "functionName": "GetCompanyProjectCLA", + "functionName": "v2.company.service.GetCompanyProjectCLA", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "authUserName": authUser.UserName, "authUserEmail": authUser.Email, "companySFID": companySFID, "projectSFID": projectSFID, } + var canSign bool resources := authUser.ResourceIDsByTypeAndRole(auth.ProjectOrganization, utils.CLADesigneeRole) projectOrg := fmt.Sprintf("%s|%s", projectSFID, companySFID) @@ -753,80 +912,198 @@ func (s *service) GetCompanyProjectCLA(ctx context.Context, authUser *auth.User, } } - // Attempt to locate the company model in our database - log.WithFields(f).Debug("locating company by SF ID") - var companyModel *v1Models.Company - companyModel, companyErr := s.companyRepo.GetCompanyByExternalID(ctx, companySFID) - if companyErr != nil { - // If we were unable to find the company/org in our local database, try to auto-create based - // on the existing SF record - if companyErr == company.ErrCompanyDoesNotExist { + // Channels for returning the results + type CompaniesResult struct { + CompanyError error + Companies []*v1Models.Company + } + companiesChannel := make(chan *CompaniesResult, 1) + + type CLAGroupsResult struct { + CLAGroupError error + CLAGroups map[string]*claGroupModel + } + claGroupsChannel := make(chan *CLAGroupsResult, 1) + + log.WithFields(f).Debug("scheduling query for companies...") + const includeChildCompanies = false // Include child/other signing entity name records? + // Separate go routine - we will get 0 or more companies (Company + separate companies for each signing entity names) + go func(ctx context.Context, companySFID string, companyID *string) { + // Attempt to locate the companyModel model in our database + log.WithFields(f).Debug("locating companyModel by SF ID") + companies, companyErr := s.companyRepo.GetCompaniesByExternalID(ctx, companySFID, includeChildCompanies) + if companyErr != nil { + // If we were unable to find the companyModel/org in our local database, try to auto-create based + // on the existing SF record + if _, ok := companyErr.(*utils.CompanyNotFound); ok { // nolint + log.WithFields(f).WithError(companyErr).Debug("companyModel not found in EasyCLA database - attempting to auto-create from platform organization service record") + companyModel, createCompanyErr := s.autoCreateCompany(ctx, companySFID) + if createCompanyErr != nil { + log.WithFields(f).WithError(createCompanyErr).Warn("problem creating companyModel from platform organization SF record") + companiesChannel <- &CompaniesResult{ + CompanyError: createCompanyErr, + Companies: nil, + } + } else if companyModel == nil { + log.WithFields(f).Warnf("problem creating companyModel from SF records - created model is nil") + companiesChannel <- &CompaniesResult{ + CompanyError: &utils.CompanyNotFound{ + Message: "unable to auto-create companyModel", + CompanySFID: companySFID, + }, + Companies: nil, + } + } else { + // Success - send the results + companiesChannel <- &CompaniesResult{ + CompanyError: nil, + Companies: []*v1Models.Company{companyModel}, + } + } + } else { + log.WithFields(f).WithError(companyErr).Warnf("problem fetching companyModel by SFID") + companiesChannel <- &CompaniesResult{ + CompanyError: companyErr, + Companies: nil, + } + } + } - log.WithFields(f).Debug("company not found in EasyCLA database - attempting to auto-create from platform organization service record") - var createCompanyErr error - companyModel, createCompanyErr = s.autoCreateCompany(ctx, companySFID) - if createCompanyErr != nil { - log.WithFields(f).Warnf("problem creating company from platform organization SF record, error: %+v", - createCompanyErr) - return nil, createCompanyErr + if companyID != nil { + log.WithFields(f).Debugf("Filtering companyModel for ID: %s ", *companyID) + index, found := findCompany(companies, *companyID) + if found { + log.WithFields(f).Debugf("Found companyModel: %v ", companies[index]) + companies = []*v1Models.Company{companies[index]} + } else { + companies = []*v1Models.Company{} } - if companyModel == nil { - log.WithFields(f).Warnf("problem creating company from SF records - created model is nil") - return nil, company.ErrCompanyDoesNotExist + } + + // Return the result through the channel + companiesChannel <- &CompaniesResult{ + CompanyError: nil, + Companies: companies, + } + }(ctx, companySFID, companyID) + + // Separate go routine + log.WithFields(f).Debug("scheduling query for CLA Groups for this project...") + go func(ctx context.Context, projectSFID string) { + claGroups, err := s.getCLAGroupsUnderProjectOrFoundation(ctx, projectSFID) + if err != nil { + log.WithFields(f).Warnf("problem fetching CLA Groups under project or foundation, error: %+v", err) + claGroupsChannel <- &CLAGroupsResult{ + CLAGroupError: err, + CLAGroups: nil, } - // Success, fall through and continue processing } else { - return nil, companyErr + claGroupsChannel <- &CLAGroupsResult{ + CLAGroupError: nil, + CLAGroups: claGroups, + } } - } + }(ctx, projectSFID) - claGroups, err := s.getCLAGroupsUnderProjectOrFoundation(ctx, projectSFID) - if err != nil { - log.WithFields(f).Warnf("problem fetching CLA Groups under project or foundation, error: %+v", err) - return nil, err + // Grab the results + log.WithFields(f).Debug("waiting for companies query to finish...") + companyResponse := <-companiesChannel + if companyResponse.CompanyError != nil { + return nil, companyResponse.CompanyError } + log.WithFields(f).Debugf("companies query finished - %d companies found", len(companyResponse.Companies)) - activeCLAList, err := s.GetCompanyProjectActiveCLAs(ctx, companyModel.CompanyID, projectSFID) - if err != nil { - log.WithFields(f).Warnf("problem fetching company project active CLAs, error: %+v", err) - return nil, err + log.WithFields(f).Debug("waiting for CLA Groups query to finish...") + claGroupResponse := <-claGroupsChannel + if claGroupResponse.CLAGroupError != nil { + return nil, claGroupResponse.CLAGroupError } + log.WithFields(f).Debugf("cla groups query finished - %d CLA Groups found", len(claGroupResponse.CLAGroups)) - resp := &models.CompanyProjectClaList{ - SignedClaList: activeCLAList.List, - UnsignedProjectList: make([]*models.UnsignedProject, 0), + // Setup another channel to fetch all these results + type CompanyProjectCLAList struct { + CompanyProjectCLAError error + CompanyProjectCLA *models.CompanyProjectCla } + companyProjectCLAChannel := make(chan *CompanyProjectCLAList, 1) - for _, activeCLA := range activeCLAList.List { - // remove cla groups for which we have signed cla - log.WithFields(f).Debugf("removing CLA Groups with active CLA, CLA Group: %+v, error: %+v", activeCLA, err) - delete(claGroups, activeCLA.ProjectID) + // For each company... + for _, companyModel := range companyResponse.Companies { + log.WithFields(f).Debugf("scheduling query for company project CLAs for company: %s...", companyModel.CompanyName) + go func(ctx context.Context, companyModel *v1Models.Company, projectSFID string, claGroups map[string]*claGroupModel) { + + // Fetch the active CLA list for this project + company + activeCLAList, err := s.GetCompanyProjectActiveCLAs(ctx, companyModel.CompanyID, projectSFID) + if err != nil { + log.WithFields(f).Warnf("problem fetching companyModel project active CLAs, error: %+v", err) + companyProjectCLAChannel <- &CompanyProjectCLAList{ + CompanyProjectCLAError: err, + CompanyProjectCLA: nil, + } + } + + // Build an inactive list... + inactiveCLAGroups := claGroups + for _, activeCLA := range activeCLAList.List { + // remove cla groups for which we have signed cla + delete(inactiveCLAGroups, activeCLA.ProjectID) + } + + var companyProjectCLA = &models.CompanyProjectCla{ + SignedClaList: activeCLAList.List, + UnsignedProjectList: make([]*models.UnsignedProject, 0), + } + + // fill details for not signed cla + for claGroupID, claGroupModel := range inactiveCLAGroups { + unsignedProject := &models.UnsignedProject{ + CompanyName: companyModel.CompanyName, + SigningEntityName: companyModel.SigningEntityName, + SigningEntityID: companyModel.CompanyID, + CanSign: canSign, + ClaGroupID: claGroupID, + ClaGroupName: claGroupModel.ClaGroupName, + ProjectName: claGroupModel.ProjectName, + ProjectSfid: claGroupModel.ProjectSFID, + SubProjects: claGroupModel.SubProjects, + IclaEnabled: claGroupModel.IclaEnabled, + CclaEnabled: claGroupModel.CclaEnabled, + } + //log.WithFields(f).Debugf("adding unsigned CLA Group: %+v, error: %+v", unsignedProject, err) + companyProjectCLA.UnsignedProjectList = append(companyProjectCLA.UnsignedProjectList, unsignedProject) + } + + companyProjectCLAChannel <- &CompanyProjectCLAList{ + CompanyProjectCLAError: err, + CompanyProjectCLA: companyProjectCLA, + } + }(ctx, companyModel, projectSFID, claGroupResponse.CLAGroups) } - // fill details for not signed cla - for claGroupID, claGroup := range claGroups { - unsignedProject := &models.UnsignedProject{ - CanSign: canSign, - ClaGroupID: claGroupID, - ClaGroupName: claGroup.ClaGroupName, - ProjectName: claGroup.ProjectName, - ProjectSfid: claGroup.ProjectSFID, - SubProjects: claGroup.SubProjects, - IclaEnabled: claGroup.IclaEnabled, - CclaEnabled: claGroup.CclaEnabled, + // Grab the results + log.WithFields(f).Debug("waiting for company project CLA results to finish...") + var companyProjectClaList []*models.CompanyProjectCla + for i := 0; i < len(companyResponse.Companies); i++ { + companyProjectCLAResponse := <-companyProjectCLAChannel + if companyProjectCLAResponse.CompanyProjectCLAError != nil { + return nil, companyProjectCLAResponse.CompanyProjectCLAError } - log.WithFields(f).Debugf("adding unsigned CLA Group: %+v, error: %+v", unsignedProject, err) - resp.UnsignedProjectList = append(resp.UnsignedProjectList, unsignedProject) + + // No error, save the value + companyProjectClaList = append(companyProjectClaList, companyProjectCLAResponse.CompanyProjectCLA) } + log.WithFields(f).Debugf("company project cla groups query finished - %d responses", len(companyResponse.Companies)) - return resp, nil + return &models.CompanyProjectClaList{ + List: companyProjectClaList, + }, nil } // GetCompanyCLAGroupManagers when provided the internal company ID and CLA Groups ID, this routine returns the list of // corresponding CLA managers func (s *service) GetCompanyCLAGroupManagers(ctx context.Context, companyID, claGroupID string) (*models.CompanyClaManagers, error) { f := logrus.Fields{ - "functionName": "GetCompanyCLAGroupManagers", + "functionName": "v2.company.service.GetCompanyCLAGroupManagers", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "companyID": companyID, "claGroupID": claGroupID, @@ -843,7 +1120,7 @@ func (s *service) GetCompanyCLAGroupManagers(ctx context.Context, companyID, cla if sigModel == nil { log.WithFields(f).Warnf("unable to query CCLA signature using Company ID: %s and CLA Group ID: %s, signed: true, approved: true - no signature found", companyID, claGroupID) - return nil, nil + return &models.CompanyClaManagers{}, nil } projectModel, projErr := s.projectRepo.GetCLAGroupByID(ctx, claGroupID, DontLoadRepoDetails) @@ -874,7 +1151,7 @@ func (s *service) GetCompanyCLAGroupManagers(ctx context.Context, companyID, cla // DB doesn't have approved_on value - just use sig created date/time ApprovedOn: sigModel.SignatureCreated, LfUsername: user.LfUsername, - Email: strfmt.Email(user.LfEmail), + Email: user.LfEmail, Name: user.Username, UserSfid: user.UserExternalID, ProjectID: sigModel.ProjectID, @@ -890,6 +1167,9 @@ func (s *service) GetCompanyCLAGroupManagers(ctx context.Context, companyID, cla func v2ProjectToMap(projectDetails *v2ProjectServiceModels.ProjectOutputDetailed) (map[string]*v2ProjectServiceModels.ProjectOutput, error) { epmap := make(map[string]*v2ProjectServiceModels.ProjectOutput) // key project_sfid + if projectDetails == nil { + return epmap, nil + } var pr v2ProjectServiceModels.ProjectOutput err := copier.Copy(&pr, projectDetails) if err != nil { @@ -902,42 +1182,50 @@ func v2ProjectToMap(projectDetails *v2ProjectServiceModels.ProjectOutputDetailed return epmap, nil } -func (s *service) getCLAGroupsUnderProjectOrFoundation(ctx context.Context, id string) (map[string]*claGroupModel, error) { +func (s *service) getCLAGroupsUnderProjectOrFoundation(ctx context.Context, projectSFID string) (map[string]*claGroupModel, error) { + f := logrus.Fields{ + "functionName": "v2.company.service.getCLAGroupsUnderProjectOrFoundation", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + } + result := make(map[string]*claGroupModel) psc := v2ProjectService.GetClient() - projectDetails, err := psc.GetProject(id) + log.WithFields(f).Debug("loading project SFID...") + projectDetails, err := psc.GetProject(projectSFID) if err != nil { return nil, err } + log.WithFields(f).Debug("loaded project SFID") + var allProjectMapping []*projects_cla_groups.ProjectClaGroup - if projectDetails.ProjectType == FoundationType { - // get all projects for all cla group under foundation - allProjectMapping, err = s.projectClaGroupsRepo.GetProjectsIdsForFoundation(id) - if err != nil { - return nil, err - } - } else { - // get cla group id from project - projectMapping, perr := s.projectClaGroupsRepo.GetClaGroupIDForProject(id) - if perr != nil { - return nil, err - } - // get all projects for that cla group - allProjectMapping, err = s.projectClaGroupsRepo.GetProjectsIdsForClaGroup(projectMapping.ClaGroupID) + + // get cla group id from project + log.WithFields(f).Debugf("projectSFID: %s is of project type", projectSFID) + projectMapping, perr := s.projectClaGroupsRepo.GetClaGroupIDForProject(ctx, projectSFID) + if perr != nil { + log.WithFields(f).WithError(perr).Warnf("unable to get CLA group IDs for project SFID: %s", projectSFID) + return nil, err + } + // get all projects for that cla group + allProjectMapping, err = s.projectClaGroupsRepo.GetProjectsIdsForClaGroup(ctx, projectMapping.ClaGroupID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get project IDs for CLA Group: %s", projectMapping.ClaGroupID) + return nil, err + } + if len(allProjectMapping) > 1 && projectDetails.Foundation != nil && projectDetails.Foundation.ID != "" { + // reload data in projectDetails for all projects of foundation + projectDetails, err = psc.GetProject(projectDetails.Foundation.ID) if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to load project from project service using SFID: %s", projectDetails.Foundation.ID) return nil, err } - if len(allProjectMapping) > 1 { - // reload data in projectDetails for all projects of foundation - projectDetails, err = psc.GetProject(projectDetails.Foundation.ID) - if err != nil { - return nil, err - } - } } + // v2ProjectMap will contains projectSFID -> salesforce details of that project v2ProjectMap, err := v2ProjectToMap(projectDetails) if err != nil { + log.WithFields(f).WithError(err).Warn("unable to convert project to project map") return nil, err } // for all cla-groups create claGroupModel @@ -963,8 +1251,9 @@ func (s *service) getCLAGroupsUnderProjectOrFoundation(ctx context.Context, id s defer wg.Done() // get cla-group info cginfo, err := s.projectRepo.GetCLAGroupByID(ctx, claGroupID, DontLoadRepoDetails) + log.WithFields(f).Debugf("clagroup info : %+v", cginfo) if err != nil || cginfo == nil { - log.Warnf("Unable to get details of cla_group: %s", claGroupID) + log.WithFields(f).Warnf("Unable to get details of cla_group: %s", claGroupID) return } claGroup.ClaGroupName = cginfo.ProjectName @@ -982,15 +1271,15 @@ func (s *service) getCLAGroupsUnderProjectOrFoundation(ctx context.Context, id s for _, spid := range claGroup.SubProjectIDs { subProject, ok := v2ProjectMap[spid] if !ok { - log.Warnf("Unable to fill details for cla_group: %s with project details of %s", claGroupID, spid) - return + log.WithFields(f).Warnf("Unable to fill details for cla_group: %s with project details of %s", claGroupID, spid) + continue } claGroup.SubProjects = append(claGroup.SubProjects, subProject.Name) } } project, ok := v2ProjectMap[pid] if !ok { - log.Warnf("Unable to fill details for cla_group: %s with project details of %s", claGroupID, claGroup.ProjectSFID) + log.WithFields(f).Warnf("Unable to fill details for cla_group: %s with project details of %s", claGroupID, claGroup.ProjectSFID) return } claGroup.ProjectLogo = project.ProjectLogo @@ -999,13 +1288,17 @@ func (s *service) getCLAGroupsUnderProjectOrFoundation(ctx context.Context, id s claGroup.ProjectSFID = pid }(id, cg) } + + log.WithFields(f).Debug("waiting for queries to finish...") wg.Wait() + log.WithFields(f).Debug("queries finished") + return result, nil } func (s *service) getAllCCLASignatures(ctx context.Context, companyID string) ([]*v1Models.Signature, error) { f := logrus.Fields{ - "functionName": "getAllCCLASignatures", + "functionName": "v2.company.service.getAllCCLASignatures", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "companyID": companyID, } @@ -1048,7 +1341,7 @@ func getUsersInfo(lfUsernames []string) (map[string]*v2UserServiceModels.User, e func fillUsersInfo(claManagers []*models.CompanyClaManager, usermap map[string]*v2UserServiceModels.User) { f := logrus.Fields{ - "functionName": "fillUsersInfo", + "functionName": "v2.company.service.fillUsersInfo", } log.WithFields(f).Debug("filling users info...") @@ -1072,7 +1365,7 @@ func fillUsersInfo(claManagers []*models.CompanyClaManager, usermap map[string]* func fillProjectInfo(claManagers []*models.CompanyClaManager, claGroups map[string]*claGroupModel) { f := logrus.Fields{ - "functionName": "fillProjectInfo", + "functionName": "v2.company.service.fillProjectInfo", } log.WithFields(f).Debug("filling project info...") for _, claManager := range claManagers { @@ -1086,30 +1379,69 @@ func fillProjectInfo(claManagers []*models.CompanyClaManager, claGroups map[stri } } -func (s *service) fillActiveCLA(wg *sync.WaitGroup, sig *v1Models.Signature, activeCla *models.ActiveCla, claGroups map[string]*claGroupModel) { +func (s *service) fillActiveCLA(ctx context.Context, wg *sync.WaitGroup, sig *v1Models.Signature, activeCla *models.ActiveCla, claGroups map[string]*claGroupModel, companyID string) { + f := logrus.Fields{ + "functionName": "v2.company.service.fillActiveCLA", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companyID": companyID, + } defer wg.Done() cg, ok := claGroups[sig.ProjectID] if !ok { - log.Warn("unable to get project details") + log.WithFields(f).Warn("unable to get project details") + return + } + + // Get Company details + v1CompanyModel, compErr := s.GetCompanyByID(ctx, companyID) + if compErr != nil { + log.WithFields(f).WithError(compErr).Warnf("unable to fetch v1CompanyModel by ID: %s ", companyID) return } + // Update acl + var acl = make([]string, 0) + if len(sig.SignatureACL) > 0 { + log.WithFields(f).Debugf("updating signature acl: %+v list for lfusernames...", sig.SignatureACL) + for _, manager := range sig.SignatureACL { + if manager.LfUsername != "" { + acl = append(acl, manager.LfUsername) + } + } + } + sort.Strings(acl) + // fill details from dynamodb - activeCla.ProjectID = sig.ProjectID + activeCla.CompanyName = v1CompanyModel.CompanyName + if v1CompanyModel.SigningEntityName == "" { + activeCla.SigningEntityName = v1CompanyModel.CompanyName + } else { + activeCla.SigningEntityName = v1CompanyModel.SigningEntityName + } + activeCla.ProjectID = sig.ProjectID // for backwards compatibility + activeCla.ClaGroupID = sig.ProjectID if sig.SignedOn == "" { activeCla.SignedOn = sig.SignatureCreated } else { activeCla.SignedOn = sig.SignedOn } + activeCla.SigningEntityID = companyID + activeCla.SignatureACL = &models.ActiveClaSignatureACL{ + UsernameList: acl, + } activeCla.ClaGroupName = cg.ClaGroupName - activeCla.SignatureID = sig.SignatureID.String() + activeCla.CompanyID = companyID + activeCla.CompanySfid = v1CompanyModel.CompanyExternalID + activeCla.SignatureID = sig.SignatureID // fill details from project service activeCla.ProjectName = cg.ProjectName activeCla.ProjectSfid = cg.ProjectSFID activeCla.ProjectType = cg.ProjectType activeCla.ProjectLogo = cg.ProjectLogo + sort.Strings(cg.SubProjects) activeCla.SubProjects = cg.SubProjects + var signatoryName string var cwg sync.WaitGroup cwg.Add(2) @@ -1118,9 +1450,9 @@ func (s *service) fillActiveCLA(wg *sync.WaitGroup, sig *v1Models.Signature, act go func() { var err error defer cwg.Done() - cclaURL, err = utils.GetDownloadLink(utils.SignedCLAFilename(sig.ProjectID, sig.SignatureType, sig.SignatureReferenceID.String(), sig.SignatureID.String())) + cclaURL, err = utils.GetDownloadLink(utils.SignedCLAFilename(sig.ProjectID, sig.SignatureType, sig.SignatureReferenceID, sig.SignatureID)) if err != nil { - log.Error("fillActiveCLA : unable to get ccla s3 link", err) + log.WithFields(f).WithError(err).Warn("fillActiveCLA : unable to get ccla s3 link", err) return } }() @@ -1131,18 +1463,7 @@ func (s *service) fillActiveCLA(wg *sync.WaitGroup, sig *v1Models.Signature, act signatoryName = sig.SignatoryName return } - usc := v2UserService.GetClient() - if len(sig.SignatureACL) == 0 { - log.Warnf("signature : %s have empty signature_acl", sig.SignatureID) - return - } - lfUsername := sig.SignatureACL[0].LfUsername - user, err := usc.GetUserByUsername(lfUsername) - if err != nil { - log.Warnf("unable to get user with lf username : %s", lfUsername) - return - } - signatoryName = user.Name + }() cwg.Wait() @@ -1182,27 +1503,27 @@ func (s *service) filterClaProjects(ctx context.Context, projects []*v2ProjectSe return results } -func fillCorporateContributorModel(wg *sync.WaitGroup, usersRepo users.UserRepository, sig *v1Models.Signature, result chan *models.CorporateContributor, searchTerm string) { +func fillCorporateContributorModel(wg *sync.WaitGroup, usersRepo users.UserRepository, sig *v1Models.Signature, result chan *models.CorporateContributor) { + f := logrus.Fields{ + "functionName": "v2.company.service.fillCorporateContributorModel", + } defer wg.Done() - user, err := usersRepo.GetUser(sig.SignatureReferenceID.String()) - if err != nil { - log.Error("fillCorporateContributorModel: unable to get user info", err) + user, err := usersRepo.GetUser(sig.SignatureReferenceID) + if err != nil || user == nil { + log.WithFields(f).Warnf("unable to load user information using signature ID: %s", sig.SignatureReferenceID) return } - if searchTerm != "" { - ls := strings.ToLower(searchTerm) - if !(strings.Contains(strings.ToLower(user.Username), ls) || strings.Contains(strings.ToLower(user.LfUsername), ls)) { - return - } - } + var contributor models.CorporateContributor var sigSignedTime = sig.SignatureCreated - contributor.GithubID = user.GithubID + contributor.GithubID = user.GithubUsername contributor.LinuxFoundationID = user.LfUsername + contributor.SignatureApproved = sig.SignatureApproved + contributor.SignatureSigned = sig.SignatureSigned contributor.Name = user.Username t, err := utils.ParseDateTime(sig.SignatureCreated) if err != nil { - log.Error("fillCorporateContributorModel: unable to parse time", err) + log.WithFields(f).WithError(err).Warn("unable to parse time") } else { sigSignedTime = utils.TimeToString(t) } @@ -1213,78 +1534,159 @@ func fillCorporateContributorModel(wg *sync.WaitGroup, usersRepo users.UserRepos result <- &contributor } -func (s *service) getAllCompanyProjectEmployeeSignatures(ctx context.Context, companySFID string, projectSFID string) ([]*v1Models.Signature, error) { +func (s *service) getAllCompanyProjectEmployeeSignatures(ctx context.Context, params *v2Ops.GetCompanyProjectContributorsParams) (*v1Models.Signatures, error) { f := logrus.Fields{ - "functionName": "getAllCompanyProjectEmployeeSignatures", + "functionName": "v2.company.service.getAllCompanyProjectEmployeeSignatures", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "companySFID": companySFID, - "projectSFID": projectSFID, + "projectSFID": params.ProjectSFID, + "companyID": params.CompanyID, } - log.WithFields(f).Debug("getAllCompanyProjectEmployeeSignatures") - comp, claGroup, err := s.getCompanyAndClaGroup(ctx, companySFID, projectSFID) + if params.SearchTerm != nil { + f["searchTerm"] = utils.StringValue(params.SearchTerm) + } + if params.PageSize != nil { + f["pageSize"] = utils.Int64Value(params.PageSize) + } + if params.NextKey != nil { + f["nextKey"] = utils.StringValue(params.NextKey) + } + + log.WithFields(f).Debug("querying company and project...") + _, claGroup, err := s.getCompanyAndClaGroup(ctx, params.CompanyID, params.ProjectSFID) if err != nil { return nil, err } - companyID := comp.CompanyID - params := v1SignatureParams.GetProjectCompanyEmployeeSignaturesParams{ + queryParams := v1SignatureParams.GetProjectCompanyEmployeeSignaturesParams{ HTTPRequest: nil, - CompanyID: companyID, + CompanyID: params.CompanyID, ProjectID: claGroup.ProjectID, } - sigs, err := s.signatureRepo.GetProjectCompanyEmployeeSignatures(ctx, params, HugePageSize) + // Pass along any query parameters from the caller + if params.PageSize != nil { + queryParams.PageSize = params.PageSize + } + if params.NextKey != nil { + queryParams.NextKey = params.NextKey + } + if params.SearchTerm != nil { + searchTermTrimmed := strings.TrimSpace(utils.StringValue(params.SearchTerm)) + log.WithFields(f).Debugf("searchTermTrimmed: %s", searchTermTrimmed) + queryParams.SearchTerm = &searchTermTrimmed + } + + sigs, err := s.signatureRepo.GetProjectCompanyEmployeeSignatures(ctx, queryParams, nil) if err != nil { return nil, err } - return sigs.Signatures, nil + return sigs, nil } // get company and project in parallel -func (s *service) getCompanyAndClaGroup(ctx context.Context, companySFID, projectSFID string) (*v1Models.Company, *v1Models.ClaGroup, error) { +func (s *service) getCompanyAndClaGroup(ctx context.Context, companyID, projectSFID string) (*v1Models.Company, *v1Models.ClaGroup, error) { f := logrus.Fields{ - "functionName": "getCompanyAndClaGroup", + "functionName": "v2.company.service.getCompanyAndClaGroup", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "companySFID": companySFID, + "companyID": companyID, "projectSFID": projectSFID, } - var comp *v1Models.Company - var claGroup *v1Models.ClaGroup - var companyErr, projectErr error - // query projects and company - var cp sync.WaitGroup - cp.Add(2) - go func() { - defer cp.Done() - comp, companyErr = s.companyRepo.GetCompanyByExternalID(ctx, companySFID) - }() - go func() { - defer cp.Done() + + type CompanyResult struct { + companyModel *v1Models.Company + Error error + } + companyResultChannel := make(chan *CompanyResult, 1) + type CLAGroupResult struct { + claGroupModel *v1Models.ClaGroup + Error error + } + claGroupResultChannel := make(chan *CLAGroupResult, 1) + + // Load the company + go func(companyID string) { + log.WithFields(f).Debugf("loading company by ID: %s", companyID) + comp, companyErr := s.companyRepo.GetCompany(ctx, companyID) + // Return the result through the channel + companyResultChannel <- &CompanyResult{ + companyModel: comp, + Error: companyErr, + } + }(companyID) + + // Load the CLA group associated with the project + go func(projectSFID string) { t := time.Now() var pm *projects_cla_groups.ProjectClaGroup - pm, projectErr = s.projectClaGroupsRepo.GetClaGroupIDForProject(projectSFID) + log.WithFields(f).Debugf("loading CLA Group by project SFID: %s", projectSFID) + pm, projectErr := s.projectClaGroupsRepo.GetClaGroupIDForProject(ctx, projectSFID) if projectErr != nil { log.WithFields(f).Debugf("cla group mapping not found for projectSFID %s", projectSFID) + // Return the result through the channel + claGroupResultChannel <- &CLAGroupResult{ + claGroupModel: nil, + Error: projectErr, + } + return + } else if pm == nil || pm.ClaGroupID == "" { + // Return the result through the channel + claGroupResultChannel <- &CLAGroupResult{ + claGroupModel: nil, + Error: &utils.ProjectCLAGroupMappingNotFound{ + ProjectSFID: projectSFID, + }, + } return } - claGroup, projectErr = s.projectRepo.GetCLAGroupByID(ctx, pm.ClaGroupID, DontLoadRepoDetails) - if claGroup == nil { - projectErr = ErrProjectNotFound + + log.WithFields(f).Debugf("loading CLA Group by ID: %s", pm.ClaGroupID) + claGroup, projectErr := s.projectRepo.GetCLAGroupByID(ctx, pm.ClaGroupID, DontLoadRepoDetails) + if projectErr != nil { + // Return the result through the channel + claGroupResultChannel <- &CLAGroupResult{ + claGroupModel: claGroup, + Error: &utils.CLAGroupNotFound{ + CLAGroupID: pm.ClaGroupID, + Err: projectErr, + }, + } + } else if claGroup == nil { + // Return the result through the channel + claGroupResultChannel <- &CLAGroupResult{ + claGroupModel: claGroup, + Error: &utils.CLAGroupNotFound{ + CLAGroupID: pm.ClaGroupID, + }, + } + } else { + claGroupResultChannel <- &CLAGroupResult{ + claGroupModel: claGroup, + Error: nil, + } + log.WithField("time_taken", time.Since(t).String()).Debugf("getting project by external id : %s completed", projectSFID) } - log.WithField("time_taken", time.Since(t).String()).Debugf("getting project by external id : %s completed", projectSFID) - }() - cp.Wait() - if companyErr != nil { - return nil, nil, companyErr + }(projectSFID) + + // Grab the results + log.WithFields(f).Debug("waiting for companies query to finish...") + companyResponse := <-companyResultChannel + if companyResponse.Error != nil { + return nil, nil, companyResponse.Error } - if projectErr != nil { - return nil, nil, projectErr + log.WithFields(f).Debug("company query finished") + + log.WithFields(f).Debug("waiting for CLA Groups query to finish...") + claGroupResponse := <-claGroupResultChannel + if claGroupResponse.Error != nil { + return nil, nil, claGroupResponse.Error } - return comp, claGroup, nil + log.WithFields(f).Debug("cla groups query finished") + + return companyResponse.companyModel, claGroupResponse.claGroupModel, nil } // autoCreateCompany helper function to create a new company record based on the SF ID and underlying record in SF func (s service) autoCreateCompany(ctx context.Context, companySFID string) (*v1Models.Company, error) { f := logrus.Fields{ - "functionName": "autoCreateCompany", + "functionName": "v2.company.service.autoCreateCompany", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "companySFID": companySFID, } @@ -1301,8 +1703,12 @@ func (s service) autoCreateCompany(ctx context.Context, companySFID string) (*v1 // If we were unable to lookup the company record in SF - we tried our best - return not exist error if sfOrgModel == nil { - log.WithFields(f).Warn("unable to locate platform organization record by SF ID - record not found") - return nil, company.ErrCompanyDoesNotExist + msg := "unable to locate platform organization record by SF ID - record not found" + log.WithFields(f).Warn(msg) + return nil, &utils.CompanyNotFound{ + Message: msg, + CompanySFID: companySFID, + } } log.WithFields(f).Debug("found platform organization record in SF") @@ -1310,7 +1716,8 @@ func (s service) autoCreateCompany(ctx context.Context, companySFID string) (*v1 companyModel, companyCreateErr := s.companyRepo.CreateCompany(ctx, &v1Models.Company{ CompanyExternalID: companySFID, CompanyName: sfOrgModel.Name, - Note: "created on-demand by v4 service based on SF Organization Service record", + + Note: "created on-demand by v4 service based on SF Organization Service record", }) if companyCreateErr != nil || companyModel == nil { @@ -1325,14 +1732,14 @@ func (s service) autoCreateCompany(ctx context.Context, companySFID string) (*v1 func (s *service) GetCompanyLookup(ctx context.Context, orgName string, websiteName string) (*models.Lookup, error) { f := logrus.Fields{ - "functionName": "company.service.GetCompanyLookup", + "functionName": "v2.company.service.GetCompanyLookup", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "orgName": orgName, "websiteName": websiteName, } orgClient := orgService.GetClient() log.WithFields(f).Debug("Looking up organization by name and website") - org, err := orgClient.SearchOrgLookup(orgName, websiteName) + org, err := orgClient.SearchOrgLookup(ctx, &orgName, &websiteName) if err != nil { log.WithFields(f).WithError(err).Warnf("unable to lookup organization by name or website") return nil, err @@ -1377,7 +1784,7 @@ func (s *service) GetCompanyLookup(ctx context.Context, orgName string, websiteN func (s *service) RequestCompanyAdmin(ctx context.Context, userID string, claManagerEmail string, claManagerName string, contributorName string, contributorEmail string, projectName string, companyName string, corporateLink string) error { orgServices := orgService.GetClient() f := logrus.Fields{ - "functionName": "RequestCompanyAdmin", + "functionName": "v2.company.service.RequestCompanyAdmin", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "companyName": companyName, "claGroupName": projectName, @@ -1399,7 +1806,7 @@ func (s *service) RequestCompanyAdmin(ctx context.Context, userID string, claMan return orgErr } if len(organizations.Data) > 0 { - msg := fmt.Sprintf("Comapny already exist with the name: %s ", companyName) + msg := fmt.Sprintf("Company already exists with the name: %s ", companyName) log.Warn(msg) return errors.New(msg) } @@ -1450,7 +1857,7 @@ func (s *service) ValidateRequestCompanyAdminCheck(ctx context.Context, f logrus return ErrClaGroupNotFound } - if errors.Is(projectErr, project.ErrProjectDoesNotExist) { + if errors.Is(projectErr, repository.ErrProjectDoesNotExist) { log.WithFields(f).WithError(projectErr).Warn("problem cla group not found") return ErrClaGroupNotFound } @@ -1477,3 +1884,12 @@ func validateRequestCompanyAdmin(userID string, claManagerName string, contribut return nil } + +func findCompany(companies []*v1Models.Company, companyID string) (int, bool) { + for index, companyModel := range companies { + if companyModel.CompanyID == companyID { + return index, true + } + } + return -1, false +} diff --git a/cla-backend-go/v2/company/service_test.go b/cla-backend-go/v2/company/service_test.go new file mode 100644 index 000000000..de5cea1d4 --- /dev/null +++ b/cla-backend-go/v2/company/service_test.go @@ -0,0 +1,182 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT +package company + +import ( + "context" + "fmt" + "testing" + + v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + v1SignatureParams "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/signatures" + v2Ops "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations/company" + + mock_company_repo "github.com/communitybridge/easycla/cla-backend-go/company/mocks" + mock_project_repo "github.com/communitybridge/easycla/cla-backend-go/project/mocks" + "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" + mock_pcg_repo "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups/mocks" + mock_signature_repo "github.com/communitybridge/easycla/cla-backend-go/signatures/mocks" + mock_user_repo "github.com/communitybridge/easycla/cla-backend-go/users/mocks" + + "github.com/golang/mock/gomock" + + "github.com/stretchr/testify/assert" +) + +func TestGetCompanyProjectContributors(t *testing.T) { + ctx := context.Background() + + testCases := []struct { + name string + signatures []*v1Models.Signature + expectedOrder []string + }{ + { + name: "With all timestamps", + signatures: []*v1Models.Signature{ + { + SignatureID: "signature-id-2", + SignatureCreated: "2021-09-13T11:59:00.981612+0000", + SignatureApproved: true, + SignatureSigned: true, + SignatureMajorVersion: "1", + SignatureMinorVersion: "0", + SignatureReferenceID: "signature_reference_id", + }, + { + SignatureID: "signature-id", + SignatureCreated: "2021-09-15T11:59:00.981612+0000", + SignatureApproved: true, + SignatureSigned: true, + SignatureMajorVersion: "1", + SignatureMinorVersion: "0", + SignatureReferenceID: "signature_reference_id", + }, + { + SignatureID: "signature-id-3", + SignatureCreated: "2021-09-14T11:59:00.981612+0000", + SignatureApproved: true, + SignatureSigned: true, + SignatureMajorVersion: "1", + SignatureMinorVersion: "0", + SignatureReferenceID: "signature_reference_id", + }, + }, + expectedOrder: []string{ + "2021-09-15T11:59:00Z", + "2021-09-14T11:59:00Z", + "2021-09-13T11:59:00Z", + }, + }, + { + name: "With empty timestamp", + signatures: []*v1Models.Signature{ + { + SignatureID: "signature-id-2", + SignatureCreated: "2021-09-13T11:59:00.981612+0000", + SignatureApproved: true, + SignatureSigned: true, + SignatureMajorVersion: "1", + SignatureMinorVersion: "0", + SignatureReferenceID: "signature_reference_id", + }, + { + SignatureID: "signature-id", + SignatureCreated: "2021-09-15T11:59:00.981612+0000", + SignatureApproved: true, + SignatureSigned: true, + SignatureMajorVersion: "1", + SignatureMinorVersion: "0", + SignatureReferenceID: "signature_reference_id", + }, + { + SignatureID: "signature-id-3", + SignatureCreated: "2021-09-14T11:59:00.981612+0000", + SignatureApproved: true, + SignatureSigned: true, + SignatureMajorVersion: "1", + SignatureMinorVersion: "0", + SignatureReferenceID: "signature_reference_id", + }, + { + SignatureID: "signature-id-4", + SignatureCreated: "", + SignatureApproved: true, + SignatureSigned: true, + SignatureMajorVersion: "1", + SignatureMinorVersion: "0", + SignatureReferenceID: "signature_reference_id_empty", + }, + }, + expectedOrder: []string{ + "2021-09-15T11:59:00Z", + "2021-09-14T11:59:00Z", + "2021-09-13T11:59:00Z", + "", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + params := v2Ops.GetCompanyProjectContributorsParams{ + CompanyID: "company-id", + ProjectSFID: "project-sfid", + } + empParams := v1SignatureParams.GetProjectCompanyEmployeeSignaturesParams{ + CompanyID: "company-id", + ProjectID: "project-id", + HTTPRequest: nil, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockProjectClaGroupRepo := mock_pcg_repo.NewMockRepository(ctrl) + mockProjectClaGroupRepo.EXPECT().GetClaGroupIDForProject(ctx, params.ProjectSFID).Return(&projects_cla_groups.ProjectClaGroup{ + ProjectSFID: "project-sfid", + ClaGroupID: "cla-group-id", + }, nil) + + mockCompanyRepo := mock_company_repo.NewMockIRepository(ctrl) + mockCompanyRepo.EXPECT().GetCompany(ctx, params.CompanyID).Return(&v1Models.Company{ + CompanyID: "company-id", + }, nil) + + mock_signature_repo := mock_signature_repo.NewMockSignatureRepository(ctrl) + mock_signature_repo.EXPECT().GetProjectCompanyEmployeeSignatures(ctx, empParams, nil).Return(&v1Models.Signatures{ + Signatures: tc.signatures, + }, nil) + + mockUserRepo := mock_user_repo.NewMockUserRepository(ctrl) + for _, sig := range tc.signatures { + mockUserRepo.EXPECT().GetUser(sig.SignatureReferenceID).Return(&v1Models.User{ + Username: "username", + GithubUsername: "github-username", + LfUsername: "lf-username", + UserID: sig.SignatureReferenceID, + }, nil) + } + + mockProjectRepo := mock_project_repo.NewMockProjectRepository(ctrl) + mockProjectRepo.EXPECT().GetCLAGroupByID(ctx, "cla-group-id", false).Return(&v1Models.ClaGroup{ + ProjectID: "project-id", + }, nil) + + service := NewService(nil, mock_signature_repo, mockProjectRepo, mockUserRepo, mockCompanyRepo, mockProjectClaGroupRepo, nil) + + response, err := service.GetCompanyProjectContributors(ctx, ¶ms) + + assert.Nil(t, err) + + fmt.Printf("response: %+v\n", response) + + assert.Equal(t, len(tc.expectedOrder), len(response.List)) + + // check the timestamp order + for i, expected := range tc.expectedOrder { + assert.Equal(t, expected, response.List[i].Timestamp) + } + }) + } +} diff --git a/cla-backend-go/v2/dynamo_events/autoenable.go b/cla-backend-go/v2/dynamo_events/autoenable.go index cf7ca2f16..48a80e7c7 100644 --- a/cla-backend-go/v2/dynamo_events/autoenable.go +++ b/cla-backend-go/v2/dynamo_events/autoenable.go @@ -10,17 +10,17 @@ import ( "strconv" "strings" - "github.com/communitybridge/easycla/cla-backend-go/utils" + service2 "github.com/communitybridge/easycla/cla-backend-go/project/service" - "github.com/communitybridge/easycla/cla-backend-go/project" + "github.com/communitybridge/easycla/cla-backend-go/utils" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" "github.com/communitybridge/easycla/cla-backend-go/github_organizations" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" "github.com/communitybridge/easycla/cla-backend-go/repositories" "github.com/go-openapi/swag" - "github.com/google/go-github/v33/github" + "github.com/google/go-github/v37/github" "github.com/sirupsen/logrus" ) @@ -40,51 +40,46 @@ type AutoEnableService interface { // NewAutoEnableService creates a new AutoEnableService func NewAutoEnableService(repositoryService repositories.Service, - githubRepo repositories.Repository, - githubOrgRepo github_organizations.Repository, + githubRepo repositories.RepositoryInterface, + githubOrgRepo github_organizations.RepositoryInterface, claRepository projects_cla_groups.Repository, - claService project.Service, + claService service2.Service, ) AutoEnableService { return &autoEnableServiceProvider{ repositoryService: repositoryService, - githubRepo: githubRepo, + gitV1Repository: githubRepo, githubOrgRepo: githubOrgRepo, claRepository: claRepository, claService: claService, } } -// autoEnableServiceProvider is an abstraction helping with managing autoEnabled flag for Github Organization +// autoEnableServiceProvider is an abstraction helping with managing autoEnabled flag for GitHub Organization // having it separated in its own struct makes testing easier. type autoEnableServiceProvider struct { repositoryService repositories.Service - githubRepo repositories.Repository - githubOrgRepo github_organizations.Repository + gitV1Repository repositories.RepositoryInterface + githubOrgRepo github_organizations.RepositoryInterface claRepository projects_cla_groups.Repository - claService project.Service + claService service2.Service } func (a *autoEnableServiceProvider) CreateAutoEnabledRepository(repo *github.Repository) (*models.GithubRepository, error) { + ctx := utils.NewContext() repositoryFullName := *repo.FullName repositoryExternalID := strconv.FormatInt(*repo.ID, 10) - f := logrus.Fields{ "functionName": "handleRepositoryAddedAction", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "repositoryFullName": repositoryFullName, } organizationName := strings.Split(repositoryFullName, "/")[0] - ctx := context.Background() - orgModel, err := a.githubOrgRepo.GetGithubOrganization(ctx, organizationName) + orgModel, err := a.githubOrgRepo.GetGitHubOrganization(ctx, organizationName) if err != nil { log.Warnf("fetching github org failed : %v", err) return nil, err } - - if !orgModel.AutoEnabled { - log.Warnf("skipping adding the repository, autoEnabled flag is off") - return nil, ErrAutoEnabledOff - } orgName := orgModel.OrganizationName claGroupID := orgModel.AutoEnabledClaGroupID @@ -106,7 +101,7 @@ func (a *autoEnableServiceProvider) CreateAutoEnabledRepository(repo *github.Rep return nil, listErr } } - claGroupModel, err := a.claRepository.GetCLAGroup(claGroupID) + claGroupModel, err := a.claRepository.GetCLAGroup(ctx, claGroupID) if err != nil { log.Warnf("fetching the cla group for cla group id : %s failed : %v", claGroupID, err) return nil, err @@ -118,18 +113,55 @@ func (a *autoEnableServiceProvider) CreateAutoEnabledRepository(repo *github.Rep } externalProjectID := claGroupModel.ProjectExternalID - - repoModel, err := a.githubRepo.AddGithubRepository(ctx, externalProjectID, projectSFID, &models.GithubRepositoryInput{ - RepositoryProjectID: swag.String(claGroupID), - RepositoryName: swag.String(repositoryFullName), - RepositoryType: swag.String("github"), - RepositoryURL: swag.String("https://github.com/" + repositoryFullName), - RepositoryOrganizationName: swag.String(organizationName), - RepositoryExternalID: swag.String(repositoryExternalID), - }) - + var repoModel *models.GithubRepository + existingRepo, err := a.repositoryService.GetRepositoryByExternalID(ctx, repositoryExternalID) if err != nil { - return nil, err + // Expecting Not found - no issue if not found - all other error we throw + if _, ok := err.(*utils.GitHubRepositoryNotFound); !ok { + return nil, err + } + if !orgModel.AutoEnabled { + log.Warnf("skipping adding the repository, autoEnabled flag is off") + return nil, ErrAutoEnabledOff + } + + repoModel, err = a.gitV1Repository.GitHubAddRepository(ctx, externalProjectID, projectSFID, &models.GithubRepositoryInput{ + RepositoryProjectID: swag.String(claGroupID), + RepositoryName: swag.String(repositoryFullName), + RepositoryType: swag.String("github"), + RepositoryURL: swag.String("https://github.com/" + repositoryFullName), + RepositoryOrganizationName: swag.String(organizationName), + RepositoryExternalID: swag.String(repositoryExternalID), + }) + + if err != nil { + return nil, err + } + } else { + // Here repository already exists. We update the same repository with latest document in order to avoid duplicate entries. + var enabled = false + if existingRepo.IsRemoteDeleted && existingRepo.WasClaEnforced { + enabled = true + } else { + enabled = existingRepo.Enabled + } + repoModel, err = a.gitV1Repository.GitHubUpdateRepository(ctx, existingRepo.RepositoryID, projectSFID, externalProjectID, &models.GithubRepositoryInput{ + RepositoryName: swag.String(repositoryFullName), + RepositoryOrganizationName: swag.String(organizationName), + RepositoryProjectID: swag.String(claGroupID), + Enabled: &enabled, + RepositoryType: swag.String("github"), + RepositoryURL: swag.String("https://github.com/" + repositoryFullName), + }) + if err != nil { + return nil, err + } + if existingRepo.IsRemoteDeleted { + err = a.gitV1Repository.GitHubSetRemoteDeletedRepository(ctx, existingRepo.RepositoryID, false, false) + if err != nil { + return nil, err + } + } } return repoModel, nil @@ -161,11 +193,11 @@ func (a *autoEnableServiceProvider) AutoEnabledForGithubOrg(f logrus.Fields, git } for _, repo := range repos.List { - if repo.RepositoryProjectID == claGroupID { + if repo.RepositoryClaGroupID == claGroupID { continue } - repo.RepositoryProjectID = claGroupID + repo.RepositoryClaGroupID = claGroupID if err := a.repositoryService.UpdateClaGroupID(context.Background(), repo.RepositoryID, claGroupID); err != nil { log.WithFields(f).Warnf("updating claGroupID for repository : %s failed : %v", repo.RepositoryID, err) return err @@ -223,13 +255,13 @@ func (a *autoEnableServiceProvider) NotifyCLAManagerForRepos(claGroupID string, // autoEnabledRepositoryEmailContent prepares the email for autoEnabled repositories func autoEnabledRepositoryEmailContent(claGroupModel *models.ClaGroup, orgName string, managers []*models.ClaManagerUser, repos []*models.GithubRepository) (string, string, []string) { claGroupName := claGroupModel.ProjectName - subject := fmt.Sprintf("EasyCLA: Auto-Enable Repository for CLA Group: %s", claGroupName) - repoPronounUpper := "Repository" + subject := fmt.Sprintf("EasyCLA: Auto-Enable CombinedRepository for CLA Group: %s", claGroupName) + repoPronounUpper := "CombinedRepository" repoPronoun := "repository" pronoun := "this " + repoPronoun repoWasHere := repoPronoun + " was" if len(repos) > 1 { - repoPronounUpper = "Repositories" + repoPronounUpper = "V3Repositories" repoPronoun = "repositories" pronoun = "these " + repoPronoun repoWasHere = repoPronoun + " were" @@ -248,7 +280,7 @@ func autoEnabledRepositoryEmailContent(claGroupModel *models.ClaGroup, orgName s Since auto-enable was configured within EasyCLA for GitHub Organization, the %s will now start enforcing CLA checks.Please verify the repository settings to ensure EasyCLA is a required check for merging Pull Requests. -See: GitHub Repository -> Settings -> Branches -> Branch Protection Rules -> Add/Edit the default branch, +See: GitHub CombinedRepository -> Settings -> Branches -> Branch Protection Rules -> Add/Edit the default branch, and confirm that 'Require status checks to pass before merging' is enabled and that EasyCLA is a required check. Additionally, consider selecting the 'Include administrators' option to enforce all configured restrictions for contributors, maintainers, and administrators.
@@ -278,7 +310,7 @@ See: GitHub Repository -> Settings -> Branches -> Branch Protection Rules -> Add // DetermineClaGroupID checks if AutoEnabledClaGroupID is set then returns it (high precedence) otherwise tries to determine // the autoEnabled claGroupID by guessing from existing repos -func DetermineClaGroupID(f logrus.Fields, gitHubOrg *models.GithubOrganization, repos *models.ListGithubRepositories) (string, error) { +func DetermineClaGroupID(f logrus.Fields, gitHubOrg *models.GithubOrganization, repos *models.GithubListRepositories) (string, error) { if gitHubOrg.AutoEnabledClaGroupID != "" { return gitHubOrg.AutoEnabledClaGroupID, nil } @@ -289,12 +321,12 @@ func DetermineClaGroupID(f logrus.Fields, gitHubOrg *models.GithubOrganization, // check if any of the repos is member to more than one cla group, in general shouldn't happen var claGroupID string for _, repo := range repos.List { - if repo.RepositoryProjectID == "" || repo.ProjectSFID == "" { + if repo.RepositoryClaGroupID == "" || repo.RepositoryProjectSfid == "" { continue } - claGroupSet[repo.RepositoryProjectID] = true - sfidSet[repo.ProjectSFID] = true - claGroupID = repo.RepositoryProjectID + claGroupID = repo.RepositoryClaGroupID + claGroupSet[repo.RepositoryClaGroupID] = true + sfidSet[repo.RepositoryProjectSfid] = true } if len(claGroupSet) == 0 && len(sfidSet) == 0 { diff --git a/cla-backend-go/v2/dynamo_events/autoenable_test.go b/cla-backend-go/v2/dynamo_events/autoenable_test.go index b92d6a8f1..fda3ce198 100644 --- a/cla-backend-go/v2/dynamo_events/autoenable_test.go +++ b/cla-backend-go/v2/dynamo_events/autoenable_test.go @@ -7,7 +7,7 @@ import ( "fmt" "testing" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -58,7 +58,7 @@ func TestAutoEnableServiceProvider_AutoEnabledForGithubOrg(t *testing.T) { m. EXPECT(). ListProjectRepositories(gomock.Any(), externalProjectID, &enabled). - Return(&models.ListGithubRepositories{}, nil) + Return(&models.GithubListRepositories{}, nil) }, }, { @@ -71,15 +71,15 @@ func TestAutoEnableServiceProvider_AutoEnabledForGithubOrg(t *testing.T) { m. EXPECT(). ListProjectRepositories(gomock.Any(), externalProjectID, &enabled). - Return(&models.ListGithubRepositories{ + Return(&models.GithubListRepositories{ List: []*models.GithubRepository{ { - RepositoryID: "d7c1050b-2f32-44ea-bad2-3c8ff980ccd4", - ProjectSFID: externalProjectID, + RepositoryID: "d7c1050b-2f32-44ea-bad2-3c8ff980ccd4", + RepositoryProjectSfid: externalProjectID, }, { - RepositoryID: "b42216b4-8f6d-41c0-8cde-7b2acbf0656a", - ProjectSFID: externalProjectID, + RepositoryID: "b42216b4-8f6d-41c0-8cde-7b2acbf0656a", + RepositoryProjectSfid: externalProjectID, }, }, }, nil) @@ -96,17 +96,17 @@ func TestAutoEnableServiceProvider_AutoEnabledForGithubOrg(t *testing.T) { m. EXPECT(). ListProjectRepositories(gomock.Any(), externalProjectID, &enabled). - Return(&models.ListGithubRepositories{ + Return(&models.GithubListRepositories{ List: []*models.GithubRepository{ { - RepositoryID: "d7c1050b-2f32-44ea-bad2-3c8ff980ccd4", - RepositoryProjectID: claGroupID, - ProjectSFID: externalProjectID, + RepositoryID: "d7c1050b-2f32-44ea-bad2-3c8ff980ccd4", + RepositoryClaGroupID: claGroupID, + RepositoryProjectSfid: externalProjectID, }, { - RepositoryID: "b42216b4-8f6d-41c0-8cde-7b2acbf0656a", - RepositoryProjectID: "anotherclagroup", - ProjectSFID: externalProjectID, + RepositoryID: "b42216b4-8f6d-41c0-8cde-7b2acbf0656a", + RepositoryClaGroupID: "anotherclagroup", + RepositoryProjectSfid: externalProjectID, }, }, }, nil) @@ -124,15 +124,15 @@ func TestAutoEnableServiceProvider_AutoEnabledForGithubOrg(t *testing.T) { m. EXPECT(). ListProjectRepositories(gomock.Any(), externalProjectID, &enabled). - Return(&models.ListGithubRepositories{ + Return(&models.GithubListRepositories{ List: []*models.GithubRepository{ { - RepositoryID: "d7c1050b-2f32-44ea-bad2-3c8ff980ccd4", - ProjectSFID: externalProjectID, + RepositoryID: "d7c1050b-2f32-44ea-bad2-3c8ff980ccd4", + RepositoryProjectSfid: externalProjectID, }, { - RepositoryID: "b42216b4-8f6d-41c0-8cde-7b2acbf0656a", - ProjectSFID: externalProjectID, + RepositoryID: "b42216b4-8f6d-41c0-8cde-7b2acbf0656a", + RepositoryProjectSfid: externalProjectID, }, }, }, nil) @@ -156,17 +156,17 @@ func TestAutoEnableServiceProvider_AutoEnabledForGithubOrg(t *testing.T) { m. EXPECT(). ListProjectRepositories(gomock.Any(), externalProjectID, &enabled). - Return(&models.ListGithubRepositories{ + Return(&models.GithubListRepositories{ List: []*models.GithubRepository{ { - RepositoryID: "d7c1050b-2f32-44ea-bad2-3c8ff980ccd4", - ProjectSFID: externalProjectID, - RepositoryProjectID: claGroupID, + RepositoryID: "d7c1050b-2f32-44ea-bad2-3c8ff980ccd4", + RepositoryProjectSfid: externalProjectID, + RepositoryClaGroupID: claGroupID, }, { - RepositoryID: "b42216b4-8f6d-41c0-8cde-7b2acbf0656a", - ProjectSFID: externalProjectID, - RepositoryProjectID: claGroupID, + RepositoryID: "b42216b4-8f6d-41c0-8cde-7b2acbf0656a", + RepositoryProjectSfid: externalProjectID, + RepositoryClaGroupID: claGroupID, }, }, }, nil) @@ -182,16 +182,16 @@ func TestAutoEnableServiceProvider_AutoEnabledForGithubOrg(t *testing.T) { m. EXPECT(). ListProjectRepositories(gomock.Any(), externalProjectID, &enabled). - Return(&models.ListGithubRepositories{ + Return(&models.GithubListRepositories{ List: []*models.GithubRepository{ { - RepositoryID: "d7c1050b-2f32-44ea-bad2-3c8ff980ccd4", - ProjectSFID: externalProjectID, + RepositoryID: "d7c1050b-2f32-44ea-bad2-3c8ff980ccd4", + RepositoryProjectSfid: externalProjectID, }, { - RepositoryID: "b42216b4-8f6d-41c0-8cde-7b2acbf0656a", - ProjectSFID: externalProjectID, - RepositoryProjectID: claGroupID, + RepositoryID: "b42216b4-8f6d-41c0-8cde-7b2acbf0656a", + RepositoryProjectSfid: externalProjectID, + RepositoryClaGroupID: claGroupID, }, }, }, nil) diff --git a/cla-backend-go/v2/dynamo_events/cla_groups_db_handler.go b/cla-backend-go/v2/dynamo_events/cla_groups_db_handler.go index 265c82e32..ac13a21b9 100644 --- a/cla-backend-go/v2/dynamo_events/cla_groups_db_handler.go +++ b/cla-backend-go/v2/dynamo_events/cla_groups_db_handler.go @@ -6,29 +6,39 @@ package dynamo_events import ( "github.com/aws/aws-lambda-go/events" log "github.com/communitybridge/easycla/cla-backend-go/logging" - "github.com/communitybridge/easycla/cla-backend-go/project" + "github.com/communitybridge/easycla/cla-backend-go/project/models" + "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/sirupsen/logrus" ) func (s *service) ProcessCLAGroupUpdateEvents(event events.DynamoDBEventRecord) error { + ctx := utils.NewContext() f := logrus.Fields{ - "functionName": "ProcessCLAGroupUpdateEvents", - "eventID": event.EventID, - "eventName": event.EventName, - "eventSource": event.EventSource, - "event": event, - "newImage": event.Change.NewImage, - "oldImage": event.Change.OldImage, + "functionName": "ProcessCLAGroupUpdateEvents", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "eventID": event.EventID, + "eventName": event.EventName, + "eventSource": event.EventSource, + "event": event, + "newImage": event.Change.NewImage, + "oldImage": event.Change.OldImage, } log.WithFields(f).Debug("processing event") - var updatedProject project.DBProjectModel + var oldProject, updatedProject models.DBProjectModel err := unmarshalStreamImage(event.Change.NewImage, &updatedProject) if err != nil { - log.WithFields(f).Warnf("unable to unmarshal project model, error: %+v", err) + log.WithFields(f).Warnf("unable to unmarshal new project model, error: %+v", err) return err } + + err = unmarshalStreamImage(event.Change.OldImage, &oldProject) + if err != nil { + log.WithFields(f).Warnf("unable to unmarshal old project model, error: %+v", err) + return err + } + log.WithFields(f).Debugf("decoded project record from stream: %+v", updatedProject) // Update any DB records that have CLA Approval Requests from Contributors - need to update Name, etc. if that has changed @@ -44,13 +54,25 @@ func (s *service) ProcessCLAGroupUpdateEvents(event events.DynamoDBEventRecord) log.WithFields(f).Warnf("unable to update cla manager request with updated CLA Group information, error: %+v", approvalListRequestErr) } + if oldProject.ProjectName != updatedProject.ProjectName { + claProjects, err := s.projectsClaGroupRepo.GetProjectsIdsForClaGroup(ctx, updatedProject.ProjectID) + if err != nil { + log.WithFields(f).Warnf("unabled to update cla group name : %v", err) + return nil + } + + for _, claProject := range claProjects { + if err := s.projectsClaGroupRepo.UpdateClaGroupName(ctx, claProject.ProjectSFID, updatedProject.ProjectName); err != nil { + log.WithFields(f).Warnf("updating cla project : %s with name : %s failed : %v", claProject.ProjectSFID, updatedProject.ProjectName, err) + return nil + } + } + log.WithFields(f).Infof("updating related cla projects with name : %s", updatedProject.ProjectName) + } + // TODO - update other tables: // cla-%s-metrics, - // cla-%s-projects-cla-groups, // cla-%s-gerrit-instances, - // possibly add/update cla_group_name/project_name to other tables: - // cla-%-repositories - // cla-%-signatures return nil } diff --git a/cla-backend-go/v2/dynamo_events/cla_manager.go b/cla-backend-go/v2/dynamo_events/cla_manager.go index 4b0d9c49f..3e489ee1c 100644 --- a/cla-backend-go/v2/dynamo_events/cla_manager.go +++ b/cla-backend-go/v2/dynamo_events/cla_manager.go @@ -19,7 +19,7 @@ import ( "github.com/sirupsen/logrus" ) -// SetInitialCLAManagerACSPermissions +// SetInitialCLAManagerACSPermissions establishes the initial CLA manager permissions func (s *service) SetInitialCLAManagerACSPermissions(ctx context.Context, signatureID string) error { f := logrus.Fields{ "functionName": "SetInitialCLAManagerACSPermissions", @@ -67,7 +67,7 @@ func (s *service) SetInitialCLAManagerACSPermissions(ctx context.Context, signat log.WithFields(f).Debugf("searching user by email: %s", sig.SignatureACL[0].LfEmail) if sig.SignatureACL[0].LfEmail != "" { - claManager, err = userServiceClient.SearchUserByEmail(sig.SignatureACL[0].LfEmail) + claManager, err = userServiceClient.SearchUsersByEmail(sig.SignatureACL[0].LfEmail.String()) if err != nil || claManager == nil { log.WithFields(f).Warnf("unable to lookup user by email: %s, error: %+v", sig.SignatureACL[0].LfEmail, err) @@ -79,7 +79,7 @@ func (s *service) SetInitialCLAManagerACSPermissions(ctx context.Context, signat // Search each one... for _, altEmail := range sig.SignatureACL[0].Emails { log.WithFields(f).Debugf("searching user by alternate email: %s", altEmail) - claManager, err = userServiceClient.SearchUserByEmail(altEmail) + claManager, err = userServiceClient.SearchUsersByEmail(altEmail) if err != nil || claManager == nil { log.WithFields(f).Warnf("unable to lookup user by alternate email: %s, error: %+v", altEmail, err) @@ -115,16 +115,16 @@ func (s *service) SetInitialCLAManagerACSPermissions(ctx context.Context, signat } log.WithFields(f).Debug("locating company record by signature reference ID...") - company, err := s.companyRepo.GetCompany(ctx, sig.SignatureReferenceID.String()) + company, err := s.companyRepo.GetCompany(ctx, sig.SignatureReferenceID) if err != nil { log.WithFields(f).Warnf("unable to lookup company by signature reference ID: %s, error: %+v", - sig.SignatureReferenceID.String(), err) + sig.SignatureReferenceID, err) return err } // fetch list of projects under cla group log.WithFields(f).Debug("locating SF projects associated with the CLA Group...") - projectList, err := s.projectsClaGroupRepo.GetProjectsIdsForClaGroup(sig.ProjectID) + projectList, err := s.projectsClaGroupRepo.GetProjectsIdsForClaGroup(ctx, sig.ProjectID) if err != nil { log.WithFields(f).Warnf("unable to fetch list of projects associated with CLA Group: %s, error: %+v", sig.ProjectID, err) @@ -166,16 +166,16 @@ func (s service) assignCLAManager(ctx context.Context, email, username, companyS return errors.New(msg) } - // check if project is signed at foundation level - foundationID := projectList[0].FoundationSFID - f["foundationID"] = projectList[0].FoundationSFID - log.WithFields(f).Debugf("using first project's foundation ID: %s", foundationID) + // // check if project is signed at foundation level + // foundationID := projectList[0].FoundationSFID + // f["foundationID"] = projectList[0].FoundationSFID + // log.WithFields(f).Debugf("using first project's foundation ID: %s", foundationID) - log.WithFields(f).Debugf("determining if this project happens to be signed at the foundation level, foundationID: %s", foundationID) - signedAtFoundation, signedErr := s.projectService.SignedAtFoundationLevel(ctx, foundationID) - if signedErr != nil { - return signedErr - } + // log.WithFields(f).Debugf("determining if this project happens to be signed at the foundation level, foundationID: %s", foundationID) + // signedAtFoundation, signedErr := s.projectService.SignedAtFoundationLevel(ctx, foundationID) + // if signedErr != nil { + // return signedErr + // } acsClient := v2AcsService.GetClient() log.WithFields(f).Debugf("locating role ID for role: %s", utils.CLAManagerRole) @@ -187,43 +187,35 @@ func (s service) assignCLAManager(ctx context.Context, email, username, companyS orgService := v2OrgService.GetClient() - if signedAtFoundation { - // add cla manager role at foundation level - err := orgService.CreateOrgUserRoleOrgScopeProjectOrg(ctx, email, foundationID, companySFID, claManagerRoleID) - if err != nil { - log.WithFields(f).Warnf("unable to add %s scope. error = %s", utils.CLAManagerRole, err) - } - } else { - projectSFIDList := utils.NewStringSet() - for _, p := range projectList { - projectSFIDList.Add(p.ProjectSFID) - } + projectSFIDList := utils.NewStringSet() + for _, p := range projectList { + projectSFIDList.Add(p.ProjectSFID) + } - var assignErr error - var wg sync.WaitGroup - wg.Add(len(projectSFIDList.List())) + var assignErr error + var wg sync.WaitGroup + wg.Add(len(projectSFIDList.List())) - // add user as cla-manager for all projects of cla-group - for _, projectSFID := range projectSFIDList.List() { - go func(projectSFID string) { - defer wg.Done() - err := orgService.CreateOrgUserRoleOrgScopeProjectOrg(ctx, email, projectSFID, companySFID, claManagerRoleID) + // add user as cla-manager for all projects of cla-group + for _, projectSFID := range projectSFIDList.List() { + go func(projectSFID string) { + defer wg.Done() + log.WithFields(f).Debugf("assigning role: %s to user: %s with email: %s for project: %s", utils.CLAManagerRole, username, email, projectSFID) + err := orgService.CreateOrgUserRoleOrgScopeProjectOrg(ctx, email, projectSFID, companySFID, claManagerRoleID) + if err != nil { + log.WithFields(f).Warnf("unable to add %s scope for project: %s, company: %s using roleID: %s for user email: %s. error = %s", + utils.CLAManagerRole, projectSFID, companySFID, claManagerRoleID, email, err) if err != nil { - log.WithFields(f).Warnf("unable to add %s scope for project: %s, company: %s using roleID: %s for user email: %s. error = %s", - utils.CLAManagerRole, projectSFID, companySFID, claManagerRoleID, email, err) - if err != nil { - assignErr = err - } + assignErr = err } - }(projectSFID) - } - - wg.Wait() + } + }(projectSFID) + } - if assignErr != nil { - return assignErr - } + wg.Wait() + if assignErr != nil { + return assignErr } return nil diff --git a/cla-backend-go/v2/dynamo_events/events.go b/cla-backend-go/v2/dynamo_events/events.go index 094aa24a3..0dcb8ab22 100644 --- a/cla-backend-go/v2/dynamo_events/events.go +++ b/cla-backend-go/v2/dynamo_events/events.go @@ -13,9 +13,10 @@ import ( // Event data model type Event struct { - EventID string `json:"event_id"` - EventProjectID string `json:"event_project_id"` - EventCompanyID string `json:"event_company_id"` + EventID string `json:"event_id"` + EventProjectID string `json:"event_project_id"` + EventCompanyID string `json:"event_company_id"` + EventCLAGroupID string `json:"event_cla_group_id"` } // should be called when we insert Event @@ -34,7 +35,7 @@ func (s *service) EventAddedEvent(event events.DynamoDBEventRecord) error { } else { companySFID = companyModel.CompanyExternalID } - pmList, err := s.projectsClaGroupRepo.GetProjectsIdsForClaGroup(newEvent.EventProjectID) + pmList, err := s.projectsClaGroupRepo.GetProjectsIdsForClaGroup(ctx, newEvent.EventCLAGroupID) if err != nil || len(pmList) == 0 { log.WithFields(f).Error("unable to get project mapping detail", err) } else { @@ -54,7 +55,7 @@ func (s *service) EventAddedEvent(event events.DynamoDBEventRecord) error { projectSFName = pmList[0].ProjectName } } - err = s.eventsRepo.AddDataToEvent(newEvent.EventID, foundationSFID, projectSFID, projectSFName, companySFID, newEvent.EventProjectID) + err = s.eventsRepo.AddDataToEvent(newEvent.EventID, foundationSFID, projectSFID, projectSFName, companySFID, newEvent.EventProjectID, newEvent.EventCLAGroupID) if err != nil { return err } diff --git a/cla-backend-go/v2/dynamo_events/github_organization.go b/cla-backend-go/v2/dynamo_events/github_organization.go index bba17a0e1..e4c973822 100644 --- a/cla-backend-go/v2/dynamo_events/github_organization.go +++ b/cla-backend-go/v2/dynamo_events/github_organization.go @@ -6,8 +6,9 @@ package dynamo_events import ( "context" + "github.com/communitybridge/easycla/cla-backend-go/github/branch_protection" + "github.com/aws/aws-lambda-go/events" - githubutils "github.com/communitybridge/easycla/cla-backend-go/github" "github.com/communitybridge/easycla/cla-backend-go/github_organizations" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/utils" @@ -17,11 +18,13 @@ import ( // GitHubOrgAddedEvent github repository added event func (s *service) GitHubOrgAddedEvent(event events.DynamoDBEventRecord) error { + ctx := utils.NewContext() f := logrus.Fields{ - "functionName": "dynamodb_events.GitHubOrgAddedEvent", - "eventName": event.EventName, - "eventSource": event.EventSource, - "eventID": event.EventID, + "functionName": "dynamodb_events.github_organization.GitHubOrgAddedEvent", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "eventName": event.EventName, + "eventSource": event.EventSource, + "eventID": event.EventID, } log.WithFields(f).Debug("processing event") @@ -35,7 +38,7 @@ func (s *service) GitHubOrgAddedEvent(event events.DynamoDBEventRecord) error { // If the branch protection value was updated from false to true.... if newGitHubOrg.BranchProtectionEnabled { log.WithFields(f).Debug("branchProtectionEnabled - processing...") - return s.enableBranchProtectionForGithubOrg(f, newGitHubOrg) + return s.enableBranchProtectionForGithubOrg(ctx, newGitHubOrg) } if newGitHubOrg.AutoEnabled { @@ -49,11 +52,13 @@ func (s *service) GitHubOrgAddedEvent(event events.DynamoDBEventRecord) error { // GitHubOrgUpdatedEvent github repository updated event func (s *service) GitHubOrgUpdatedEvent(event events.DynamoDBEventRecord) error { + ctx := utils.NewContext() f := logrus.Fields{ - "functionName": "dynamodb_events.GitHubOrgUpdatedEvent", - "eventName": event.EventName, - "eventSource": event.EventSource, - "eventID": event.EventID, + "functionName": "dynamodb_events.github_organization.GitHubOrgUpdatedEvent", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "eventName": event.EventName, + "eventSource": event.EventSource, + "eventID": event.EventID, } log.WithFields(f).Debug("processing event") @@ -72,7 +77,7 @@ func (s *service) GitHubOrgUpdatedEvent(event events.DynamoDBEventRecord) error // If the branch protection value was updated from false to true.... if !oldGitHubOrg.BranchProtectionEnabled && newGitHubOrg.BranchProtectionEnabled { log.WithFields(f).Debug("transition of branchProtectionEnabled false => true - processing...") - return s.enableBranchProtectionForGithubOrg(f, newGitHubOrg) + return s.enableBranchProtectionForGithubOrg(ctx, newGitHubOrg) } if !oldGitHubOrg.AutoEnabled && newGitHubOrg.AutoEnabled { @@ -87,11 +92,11 @@ func (s *service) GitHubOrgUpdatedEvent(event events.DynamoDBEventRecord) error func (s *service) GitHubOrgDeletedEvent(event events.DynamoDBEventRecord) error { ctx := utils.NewContext() f := logrus.Fields{ - "functionName": "dynamodb_events.GitHubOrgDeletedEvent", + "functionName": "dynamodb_events.github_organization.GitHubOrgDeletedEvent", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "eventName": event.EventName, "eventSource": event.EventSource, "eventID": event.EventID, - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), } log.WithFields(f).Debug("processing event") @@ -127,7 +132,19 @@ func (s *service) GitHubOrgDeletedEvent(event events.DynamoDBEventRecord) error return nil } -func (s *service) enableBranchProtectionForGithubOrg(f logrus.Fields, newGitHubOrg github_organizations.GithubOrganization) error { +func (s *service) enableBranchProtectionForGithubOrg(ctx context.Context, newGitHubOrg github_organizations.GithubOrganization) error { + f := logrus.Fields{ + "functionName": "dynamo_events.github_organization.enableBranchProtectionForGithubOrg", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": newGitHubOrg.ProjectSFID, + "organizationName": newGitHubOrg.OrganizationName, + "organizationSFID": newGitHubOrg.OrganizationSFID, + "organizationInstallationID": newGitHubOrg.OrganizationInstallationID, + "autoEnabled": newGitHubOrg.AutoEnabled, + "branchProtectionEnabled": newGitHubOrg.BranchProtectionEnabled, + "autoEnabledCLAGroupID": newGitHubOrg.AutoEnabledClaGroupID, + } + // Locate the repositories already saved under this organization log.WithFields(f).Debugf("loading repositories under the organization : %s", newGitHubOrg.OrganizationName) repos, err := s.repositoryService.GetRepositoriesByOrganizationName(context.Background(), newGitHubOrg.OrganizationName) @@ -136,15 +153,13 @@ func (s *service) enableBranchProtectionForGithubOrg(f logrus.Fields, newGitHubO return err } - ctx := context.Background() log.WithFields(f).Debugf("creating a new GitHub client object for org: %s...", newGitHubOrg.OrganizationName) - gitHubClient, clientErr := githubutils.NewGithubAppClient(newGitHubOrg.OrganizationInstallationID) - if clientErr != nil { - return clientErr + branchProtectionRepo, err := branch_protection.NewBranchProtectionRepository(newGitHubOrg.OrganizationInstallationID, branch_protection.EnableBlockingLimiter()) + if err != nil { + log.WithFields(f).WithError(err).Warnf("initializing branch protection repository failed") + return err } - branchProtectionRepo := githubutils.NewBranchProtectionRepository(gitHubClient.Repositories, githubutils.EnableBlockingLimiter()) - var eg errgroup.Group // a pool of 5 concurrent workers var workerTokens = make(chan struct{}, 5) @@ -160,16 +175,10 @@ func (s *service) enableBranchProtectionForGithubOrg(f logrus.Fields, newGitHubO }() log.WithFields(f).Debugf("enabling branch protection for repository: %s", repo.RepositoryName) - log.WithFields(f).Debugf("looking up the default branch for the GitHub repository: %s...", repo.RepositoryName) - defaultBranch, branchErr := branchProtectionRepo.GetDefaultBranchForRepo(ctx, newGitHubOrg.OrganizationName, repo.RepositoryName) - if branchErr != nil { - return branchErr - } - log.WithFields(f).Debugf("enabling branch protection on the default branch %s for the GitHub repository: %s...", - defaultBranch, repo.RepositoryName) + utils.GithubBranchProtectionPatternAll, repo.RepositoryName) return branchProtectionRepo.EnableBranchProtection(ctx, newGitHubOrg.OrganizationName, repo.RepositoryName, - defaultBranch, true, []string{utils.GitHubBotName}, []string{}) + utils.GithubBranchProtectionPatternAll, true, []string{utils.GitHubBotName}, []string{}) }) } diff --git a/cla-backend-go/v2/dynamo_events/github_repository.go b/cla-backend-go/v2/dynamo_events/github_repository.go index 9fa9852e0..dc2e16988 100644 --- a/cla-backend-go/v2/dynamo_events/github_repository.go +++ b/cla-backend-go/v2/dynamo_events/github_repository.go @@ -6,7 +6,8 @@ package dynamo_events import ( "context" - "github.com/communitybridge/easycla/cla-backend-go/github" + "github.com/communitybridge/easycla/cla-backend-go/github/branch_protection" + "github.com/communitybridge/easycla/cla-backend-go/repositories" "github.com/sirupsen/logrus" @@ -15,7 +16,7 @@ import ( "github.com/communitybridge/easycla/cla-backend-go/utils" ) -// GithubRepoModifyEvent github repository modify event +// GithubRepoModifyAddEvent github repository modify add event func (s *service) GithubRepoModifyAddEvent(event events.DynamoDBEventRecord) error { ctx := utils.NewContext() f := logrus.Fields{ @@ -40,7 +41,7 @@ func (s *service) GithubRepoModifyAddEvent(event events.DynamoDBEventRecord) err log.WithFields(f).Warnf("problem unmarshalling old repository model event, error: %+v", err) return err } - claGroupID = oldRepoModel.RepositoryProjectID + claGroupID = oldRepoModel.RepositoryCLAGroupID projectSFID = oldRepoModel.ProjectSFID parentProjectSFID = oldRepoModel.RepositorySfdcID } else { @@ -50,7 +51,7 @@ func (s *service) GithubRepoModifyAddEvent(event events.DynamoDBEventRecord) err log.WithFields(f).Warnf("problem unmarshalling the new repository model event, error: %+v", err) return err } - claGroupID = newRepoModel.RepositoryProjectID + claGroupID = newRepoModel.RepositoryCLAGroupID projectSFID = newRepoModel.ProjectSFID parentProjectSFID = newRepoModel.RepositorySfdcID } @@ -96,7 +97,7 @@ func (s *service) EnableBranchProtectionServiceHandler(event events.DynamoDBEven parentOrgName := newRepoModel.RepositoryOrganizationName log.WithFields(f).Warnf("problem locating github organization by name: %s, error: %+v", parentOrgName, err) - gitHubOrg, err := s.githubOrgService.GetGithubOrganizationByName(context.Background(), parentOrgName) + gitHubOrg, err := s.githubOrgService.GetGitHubOrganizationByName(context.Background(), parentOrgName) if err != nil { log.WithFields(f).Warnf("problem locating github organization by name: %s, error: %+v", parentOrgName, err) return nil @@ -110,25 +111,17 @@ func (s *service) EnableBranchProtectionServiceHandler(event events.DynamoDBEven log.WithFields(f).Debug("branch protection is enabled for this organization") ctx := context.Background() - log.WithFields(f).Debug("creating a new GitHub client object...") - gitHubClient, clientErr := github.NewGithubAppClient(gitHubOrg.OrganizationInstallationID) - if clientErr != nil { - return clientErr - } - - branchProtectionRepository := github.NewBranchProtectionRepository(gitHubClient.Repositories, github.EnableBlockingLimiter()) - - log.WithFields(f).Debug("looking up the default branch for the GitHub repository...") - defaultBranch, branchErr := branchProtectionRepository.GetDefaultBranchForRepo(ctx, gitHubOrg.OrganizationName, newRepoModel.RepositoryName) - if branchErr != nil { - return branchErr + branchProtectionRepository, err := branch_protection.NewBranchProtectionRepository(gitHubOrg.OrganizationInstallationID, branch_protection.EnableBlockingLimiter()) + if err != nil { + log.WithFields(f).WithError(err).Warnf("initializing branch protection repository failed") + return err } log.WithFields(f).Debugf("enabling branch protection on th default branch %s for the GitHub repository: %s...", - defaultBranch, newRepoModel.RepositoryName) + utils.GithubBranchProtectionPatternAll, newRepoModel.RepositoryName) return branchProtectionRepository.EnableBranchProtection(ctx, parentOrgName, newRepoModel.RepositoryName, - defaultBranch, true, []string{utils.GitHubBotName}, []string{}) + utils.GithubBranchProtectionPatternAll, true, []string{utils.GitHubBotName}, []string{}) } log.WithFields(f).Debug("github organization branch protection is not enabled - no action required") @@ -157,7 +150,7 @@ func (s *service) DisableBranchProtectionServiceHandler(event events.DynamoDBEve // Branch protection only available for GitHub if oldRepoModel.RepositoryType == utils.GitHubType { parentOrgName := oldRepoModel.RepositoryOrganizationName - gitHubOrg, err := s.githubOrgService.GetGithubOrganizationByName(context.Background(), parentOrgName) + gitHubOrg, err := s.githubOrgService.GetGitHubOrganizationByName(context.Background(), parentOrgName) if err != nil { log.WithFields(f).Warnf("problem locating github organization by name: %s, error: %+v", parentOrgName, err) return nil @@ -208,7 +201,7 @@ func (s *service) setRepositoryCount(ctx context.Context, claGroupID string, par // Update projects-cla-group table log.WithFields(f).Debugf("Updating the projects-cla-groups-table for projectSFID: %s ", projectSFID) - pcgErr := s.projectsClaGroupRepo.UpdateRepositoriesCount(projectSFID, int64(repoCount), true) + pcgErr := s.projectsClaGroupRepo.UpdateRepositoriesCount(ctx, projectSFID, int64(repoCount), true) if pcgErr != nil { log.WithFields(f).WithError(updateErr).Debugf("Failed to set repositories_count for project: %s ", projectSFID) return pcgErr diff --git a/cla-backend-go/v2/dynamo_events/gitlab_branch_protection.go b/cla-backend-go/v2/dynamo_events/gitlab_branch_protection.go new file mode 100644 index 000000000..8a96cb0ef --- /dev/null +++ b/cla-backend-go/v2/dynamo_events/gitlab_branch_protection.go @@ -0,0 +1,145 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package dynamo_events + +import ( + "context" + "fmt" + "strconv" + + "github.com/aws/aws-lambda-go/events" + gitlab_api "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/communitybridge/easycla/cla-backend-go/v2/common" + "github.com/sirupsen/logrus" + "golang.org/x/sync/errgroup" +) + +// GitLabOrgUpdatedEvent handles branch protection functionality +func (s *service) GitLabOrgUpdatedEvent(event events.DynamoDBEventRecord) error { + ctx := utils.NewContext() + f := logrus.Fields{ + "functionName": "dynamodb_events.gitlab_organization.GitLabOrgUpdatedEvent", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "eventName": event.EventName, + "eventSource": event.EventSource, + "eventID": event.EventID, + } + + log.WithFields(f).Debug("processing event") + var newGitLabOrg, oldGitLabOrg common.GitLabOrganization + err := unmarshalStreamImage(event.Change.NewImage, &newGitLabOrg) + if err != nil { + log.WithFields(f).Warnf("problem unmarshalling the new gitlab organization model from the updated event, error: %+v", err) + return err + } + err = unmarshalStreamImage(event.Change.OldImage, &oldGitLabOrg) + if err != nil { + log.WithFields(f).Warnf("problem unmarshalling the old gitlab organization model from the updated event, error: %+v", err) + return err + } + + f["gitlabOrgID"] = newGitLabOrg.OrganizationID + f["gitlabOrgName"] = newGitLabOrg.OrganizationName + + if !newGitLabOrg.Enabled { + log.WithFields(f).Debugf("gitlab org is not enabled, nothing to do this time") + return nil + } + + // If the branch protection value was updated from false to true.... + if !oldGitLabOrg.BranchProtectionEnabled && newGitLabOrg.BranchProtectionEnabled { + log.WithFields(f).Debug("transition of branchProtectionEnabled false => true - processing...") + return s.enableBranchProtectionForGitLabOrg(ctx, newGitLabOrg) + } + + // it might be a new gitlab org that was just authenticated + if oldGitLabOrg.AuthInfo != newGitLabOrg.AuthInfo && newGitLabOrg.BranchProtectionEnabled { + log.WithFields(f).Debug("auth info was set for the org, processing the branch protection") + return s.enableBranchProtectionForGitLabOrg(ctx, newGitLabOrg) + } + + log.WithFields(f).Debug("no transition of branchProtectionEnabled false => true - ignoring...") + return nil +} + +func (s *service) enableBranchProtectionForGitLabOrg(ctx context.Context, newGitLabOrg common.GitLabOrganization) error { + f := logrus.Fields{ + "functionName": "dynamo_events.gitlab_organization.enableBranchProtectionForGitLabOrg", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": newGitLabOrg.ProjectSFID, + "organizationName": newGitLabOrg.OrganizationName, + "organizationSFID": newGitLabOrg.OrganizationSFID, + "autoEnabled": newGitLabOrg.AutoEnabled, + "branchProtectionEnabled": newGitLabOrg.BranchProtectionEnabled, + } + + gitlabOrg, err := s.gitLabOrgRepo.GetGitLabOrganizationByName(ctx, newGitLabOrg.OrganizationName) + if err != nil { + return fmt.Errorf("fetching gitlab org : %s failed : %v", newGitLabOrg.OrganizationName, err) + } + + oauthResponse, err := s.gitLabOrgService.RefreshGitLabOrganizationAuth(ctx, gitlabOrg) + if err != nil { + return fmt.Errorf("refreshing gitlab org auth failed : %v", err) + } + + log.WithFields(f).Debugf("creating a new gitlab client object for org: %s...", newGitLabOrg.OrganizationName) + gitLabClient, err := gitlab_api.NewGitlabOauthClient(*oauthResponse, s.gitLabApp) + if err != nil { + return fmt.Errorf("initializing GitLab client failed : %v", err) + } + + // Locate the repositories already saved under this organization + log.WithFields(f).Debugf("loading repositories under the organization : %s", newGitLabOrg.OrganizationName) + repos, err := s.v2Repository.GitLabGetRepositoriesByOrganizationName(context.Background(), newGitLabOrg.OrganizationName) + if err != nil { + log.WithFields(f).Warnf("problem locating repositories by organization name, error: %+v", err) + return err + } + + var eg errgroup.Group + // a pool of 5 concurrent workers + var workerTokens = make(chan struct{}, 5) + for _, repo := range repos { + // this is for goroutine local variables + repo := repo + // acquire a worker token to create a new goroutine + workerTokens <- struct{}{} + // Update the branch protection in a go routine... + eg.Go(func() error { + defer func() { + <-workerTokens // release the workerToken + }() + log.WithFields(f).Debugf("enabling branch protection for repository: %s", repo.RepositoryName) + + repositoryExternalIDInt, err := strconv.Atoi(repo.RepositoryExternalID) + if err != nil { + return fmt.Errorf("parsing external repository id failed : %v", err) + } + + gitlabDefaultBranch, err := gitlab_api.GetDefaultBranch(gitLabClient, repositoryExternalIDInt) + if err != nil { + return fmt.Errorf("fetching default branch failed : %v", err) + } + + err = gitlab_api.SetOrCreateBranchProtection(ctx, gitLabClient, repositoryExternalIDInt, gitlabDefaultBranch.Name) + if err != nil { + return fmt.Errorf("enabling branch protection for pattern : %s, failed : %v", gitlabDefaultBranch.Name, err) + } + return nil + }) + } + + // Wait for the go routines to finish + log.WithFields(f).Debugf("waiting for %d repositories to complete...", len(repos)) + var branchProtectionErr error + if loadErr := eg.Wait(); loadErr != nil { + log.WithFields(f).Warnf("encountered branch protection setup error: %+v", loadErr) + branchProtectionErr = loadErr + } + + return branchProtectionErr +} diff --git a/cla-backend-go/v2/dynamo_events/gitlab_webhooks.go b/cla-backend-go/v2/dynamo_events/gitlab_webhooks.go new file mode 100644 index 000000000..0b3163ac5 --- /dev/null +++ b/cla-backend-go/v2/dynamo_events/gitlab_webhooks.go @@ -0,0 +1,232 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package dynamo_events + +import ( + "fmt" + "strconv" + + "github.com/aws/aws-lambda-go/events" + "github.com/communitybridge/easycla/cla-backend-go/config" + gitlab_api "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/repositories" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/sirupsen/logrus" +) + +func (s *service) GitLabRepoAddedWebhookEventHandler(event events.DynamoDBEventRecord) error { + ctx := utils.NewContext() + f := logrus.Fields{ + "functionName": "GitLabRepoAddedWebhookEventHandler", + "eventID": event.EventID, + "eventName": event.EventName, + "eventSource": event.EventSource, + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + var newRepoModel repositories.RepositoryDBModel + + log.WithFields(f).Debugf("processing record %s event...", event.EventName) + err := unmarshalStreamImage(event.Change.NewImage, &newRepoModel) + if err != nil { + log.WithFields(f).Warnf("problem unmarshalling the new repository model event, error: %+v", err) + return err + } + + if !s.isGitlabRepo(log.WithFields(f), &newRepoModel) { + return nil + } + + if !newRepoModel.Enabled { + log.WithFields(f).Debugf("gitlab repo is not enabled, nothing to do at this point") + return nil + } + + repositoryID := newRepoModel.RepositoryID + repositoryName := newRepoModel.RepositoryName + repositoryExternalID := newRepoModel.RepositoryExternalID + + log.WithFields(f).Debugf("adding webhook for repository : %s:%s with external id : %s", repositoryID, repositoryName, repositoryExternalID) + + gitlabOrg, err := s.gitLabOrgRepo.GetGitLabOrganizationByName(ctx, newRepoModel.RepositoryOrganizationName) + if err != nil { + return fmt.Errorf("fetching gitlab org : %s failed : %v", newRepoModel.RepositoryOrganizationName, err) + } + + oauthResponse, err := s.gitLabOrgService.RefreshGitLabOrganizationAuth(ctx, gitlabOrg) + if err != nil { + return fmt.Errorf("refreshing gitlab org auth failed : %v", err) + } + + gitLabClient, err := gitlab_api.NewGitlabOauthClient(*oauthResponse, s.gitLabApp) + if err != nil { + return fmt.Errorf("initializing GitLab client failed : %v", err) + } + + repositoryExternalIDInt, err := strconv.Atoi(repositoryExternalID) + if err != nil { + return fmt.Errorf("parsing external repository id failed : %v", err) + } + + conf := config.GetConfig() + if err := gitlab_api.SetWebHook(gitLabClient, conf.Gitlab.WebHookURI, repositoryExternalIDInt, gitlabOrg.AuthState); err != nil { + log.WithFields(f).Errorf("adding gitlab webhook failed : %v", err) + } + log.WithFields(f).Debugf("gitlab webhhok added succesfully for repository") + + log.WithFields(f).Debugf("enabling gitlab pipeline protection if not alreasy") + return gitlab_api.EnableMergePipelineProtection(ctx, gitLabClient, repositoryExternalIDInt) +} + +func (s *service) GitlabRepoModifiedWebhookEventHandler(event events.DynamoDBEventRecord) error { + ctx := utils.NewContext() + f := logrus.Fields{ + "functionName": "GitlabRepoModifiedWebhookEventHandler", + "eventID": event.EventID, + "eventName": event.EventName, + "eventSource": event.EventSource, + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + var newRepoModel repositories.RepositoryDBModel + var oldRepoModel repositories.RepositoryDBModel + + log.WithFields(f).Debugf("processing record %s event...", event.EventName) + err := unmarshalStreamImage(event.Change.OldImage, &oldRepoModel) + if err != nil { + log.WithFields(f).Warnf("problem unmarshalling the new repository model event, error: %+v", err) + return err + } + + err = unmarshalStreamImage(event.Change.NewImage, &newRepoModel) + if err != nil { + log.WithFields(f).Warnf("problem unmarshalling the old repository model event, error: %+v", err) + return err + } + + if !s.isGitlabRepo(log.WithFields(f), &newRepoModel) { + return nil + } + + if newRepoModel.Enabled == oldRepoModel.Enabled { + log.WithFields(f).Debugf("only changes of Enabled field are processed") + return nil + } + + repositoryID := oldRepoModel.RepositoryID + repositoryName := oldRepoModel.RepositoryName + repositoryExternalID := oldRepoModel.RepositoryExternalID + + if newRepoModel.Enabled { + log.WithFields(f).Debugf("adding webhook for repository : %s:%s with external id : %s", repositoryID, repositoryName, repositoryExternalID) + } else { + log.WithFields(f).Debugf("removing webhook for repository : %s:%s with external id : %s", repositoryID, repositoryName, repositoryExternalID) + } + + gitlabOrg, err := s.gitLabOrgRepo.GetGitLabOrganizationByName(ctx, oldRepoModel.RepositoryOrganizationName) + if err != nil { + return fmt.Errorf("fetching gitlab org : %s failed : %v", oldRepoModel.RepositoryOrganizationName, err) + } + + oauthResponse, err := s.gitLabOrgService.RefreshGitLabOrganizationAuth(ctx, gitlabOrg) + if err != nil { + return fmt.Errorf("refreshing gitlab org auth failed : %v", err) + } + + gitLabClient, err := gitlab_api.NewGitlabOauthClient(*oauthResponse, s.gitLabApp) + if err != nil { + return fmt.Errorf("initializing GitLab client failed : %v", err) + } + + repositoryExternalIDInt, err := strconv.Atoi(repositoryExternalID) + if err != nil { + return fmt.Errorf("parding external repository id failed : %v", err) + } + + conf := config.GetConfig() + + if newRepoModel.Enabled { + if err := gitlab_api.SetWebHook(gitLabClient, conf.Gitlab.WebHookURI, repositoryExternalIDInt, gitlabOrg.AuthState); err != nil { + log.WithFields(f).Errorf("adding gitlab webhook failed : %v", err) + } + log.WithFields(f).Debugf("enabling gitlab pipeline protection if not alreasy") + if err := gitlab_api.EnableMergePipelineProtection(ctx, gitLabClient, repositoryExternalIDInt); err != nil { + return err + } + } else { + if err := gitlab_api.RemoveWebHook(gitLabClient, conf.Gitlab.WebHookURI, repositoryExternalIDInt); err != nil { + log.WithFields(f).Errorf("removing gitlab webhook failed : %v", err) + } + } + + log.WithFields(f).Debugf("gitlab webhhok processed succesfully for repository") + return nil +} + +func (s *service) GitLabRepoRemovedWebhookEventHandler(event events.DynamoDBEventRecord) error { + ctx := utils.NewContext() + f := logrus.Fields{ + "functionName": "GitLabRepoRemovedWebhookEventHandler", + "eventID": event.EventID, + "eventName": event.EventName, + "eventSource": event.EventSource, + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + var oldRepoModel repositories.RepositoryDBModel + + log.WithFields(f).Debugf("processing record %s event...", event.EventName) + err := unmarshalStreamImage(event.Change.OldImage, &oldRepoModel) + if err != nil { + log.WithFields(f).Warnf("problem unmarshalling the old repository model event, error: %+v", err) + return err + } + + if !s.isGitlabRepo(log.WithFields(f), &oldRepoModel) { + return nil + } + + repositoryID := oldRepoModel.RepositoryID + repositoryName := oldRepoModel.RepositoryName + repositoryExternalID := oldRepoModel.RepositoryExternalID + + log.WithFields(f).Debugf("removing webhook for repository : %s:%s with external id : %s", repositoryID, repositoryName, repositoryExternalID) + + gitlabOrg, err := s.gitLabOrgRepo.GetGitLabOrganizationByName(ctx, oldRepoModel.RepositoryOrganizationName) + if err != nil { + return fmt.Errorf("fetching gitlab org : %s failed : %v", oldRepoModel.RepositoryOrganizationName, err) + } + + oauthResponse, err := s.gitLabOrgService.RefreshGitLabOrganizationAuth(ctx, gitlabOrg) + if err != nil { + return fmt.Errorf("refreshing gitlab org auth failed : %v", err) + } + + gitLabClient, err := gitlab_api.NewGitlabOauthClient(*oauthResponse, s.gitLabApp) + if err != nil { + return fmt.Errorf("initializing GitLab client failed : %v", err) + } + + repositoryExternalIDInt, err := strconv.Atoi(repositoryExternalID) + if err != nil { + return fmt.Errorf("parding external repository id failed : %v", err) + } + + conf := config.GetConfig() + if err := gitlab_api.RemoveWebHook(gitLabClient, conf.Gitlab.WebHookURI, repositoryExternalIDInt); err != nil { + log.WithFields(f).Errorf("removing gitlab webhook failed : %v", err) + } + + log.WithFields(f).Debugf("gitlab webhhok removed succesfully for repository") + return nil +} + +func (s *service) isGitlabRepo(logEntry *logrus.Entry, repoModel *repositories.RepositoryDBModel) bool { + if repoModel.RepositoryType != utils.GitLabLower { + logEntry.Debugf("only processing gitlab instances") + return false + } + return true +} diff --git a/cla-backend-go/v2/dynamo_events/projects_cla_groups.go b/cla-backend-go/v2/dynamo_events/projects_cla_groups.go index 13cfeaa5f..dbff5ff47 100644 --- a/cla-backend-go/v2/dynamo_events/projects_cla_groups.go +++ b/cla-backend-go/v2/dynamo_events/projects_cla_groups.go @@ -4,13 +4,13 @@ package dynamo_events import ( + "context" "fmt" "strings" "sync" - "time" "github.com/aws/aws-sdk-go/aws" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/signatures" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/signatures" organizationService "github.com/communitybridge/easycla/cla-backend-go/v2/organization-service" userService "github.com/communitybridge/easycla/cla-backend-go/v2/user-service" @@ -18,7 +18,7 @@ import ( "github.com/aws/aws-lambda-go/events" claEvents "github.com/communitybridge/easycla/cla-backend-go/events" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/utils" v2ProjectService "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" @@ -33,131 +33,150 @@ type ProjectClaGroup struct { RepositoriesCount int64 `json:"repositories_count"` } -// ProjectServiceEnableCLAServiceHandler handles enabling the CLA Service attribute from the project service -func (s *service) ProjectServiceEnableCLAServiceHandler(event events.DynamoDBEventRecord) error { - f := logrus.Fields{ - "functionName": "ProjectServiceEnableCLAServiceHandler", - "eventID": event.EventID, - "eventName": event.EventName, - "eventSource": event.EventSource, - } - - log.WithFields(f).Debug("processing request") - var newProject ProjectClaGroup - err := unmarshalStreamImage(event.Change.NewImage, &newProject) - if err != nil { - log.WithFields(f).WithError(err).Warn("project decoding add event") - return err - } - - f["projectSFID"] = newProject.ProjectSFID - f["claGroupID"] = newProject.ClaGroupID - f["foundationSFID"] = newProject.FoundationSFID - - psc := v2ProjectService.GetClient() - log.WithFields(f).Debug("enabling CLA service...") - projectDetails, prjerr := psc.GetProject(newProject.ProjectSFID) - if prjerr != nil { - log.WithError(err).Warn("enable to get project details") - } - projectName := newProject.ProjectSFID - if projectDetails != nil { - projectName = projectDetails.Name - } - start, _ := utils.CurrentTime() - err = psc.EnableCLA(newProject.ProjectSFID) - if err != nil { - log.WithFields(f).WithError(err).Warn("enabling CLA service failed") - return err - } - finish, _ := utils.CurrentTime() - log.WithFields(f).Debugf("enabling CLA service completed - took: %s", finish.Sub(start).String()) - - // Log the event - eventErr := s.eventsRepo.CreateEvent(&models.Event{ - ContainsPII: false, - EventData: fmt.Sprintf("enabled CLA service for project: %s", projectName), - EventSummary: fmt.Sprintf("enabled CLA service for project: %s", projectName), - EventFoundationSFID: newProject.FoundationSFID, - EventProjectExternalID: newProject.ProjectSFID, - EventProjectID: newProject.ClaGroupID, - EventProjectSFID: newProject.ProjectSFID, - EventType: claEvents.ProjectServiceCLAEnabled, - LfUsername: "easycla system", - UserID: "easycla system", - UserName: "easycla system", - // EventProjectName: "", - EventProjectSFName: projectName, - }) - if eventErr != nil { - log.WithFields(f).WithError(eventErr).Warn("problem logging event for enabling CLA service") - // Ok - don't fail for now - } - - return nil -} - -// ProjectServiceDisableCLAServiceHandler handles disabling/removing the CLA Service attribute from the project service -func (s *service) ProjectServiceDisableCLAServiceHandler(event events.DynamoDBEventRecord) error { - f := logrus.Fields{ - "functionName": "ProjectServiceDisableCLAServiceHandler", - "eventID": event.EventID, - "eventName": event.EventName, - "eventSource": event.EventSource, - } - - log.WithFields(f).Debug("processing request") - var oldProject ProjectClaGroup - err := unmarshalStreamImage(event.Change.OldImage, &oldProject) - if err != nil { - log.WithFields(f).WithError(err).Warn("problem unmarshalling stream image") - return err - } - - // Add more fields for the logger - f["ProjectSFID"] = oldProject.ProjectSFID - f["ClaGroupID"] = oldProject.ClaGroupID - f["FoundationSFID"] = oldProject.FoundationSFID - - psc := v2ProjectService.GetClient() - // Gathering metrics - grab the time before the API call - before, _ := utils.CurrentTime() - log.WithFields(f).Debug("disabling CLA service") - err = psc.DisableCLA(oldProject.ProjectSFID) - if err != nil { - log.WithFields(f).WithError(err).Warn("disabling CLA service failed") - return err - } - log.WithFields(f).Debugf("disabling CLA service took %s", time.Since(before).String()) - - // Log the event - eventErr := s.eventsRepo.CreateEvent(&models.Event{ - ContainsPII: false, - EventData: fmt.Sprintf("disabled CLA service for project: %s", oldProject.ProjectSFID), - EventSummary: fmt.Sprintf("disabled CLA service for project: %s", oldProject.ProjectSFID), - EventFoundationSFID: oldProject.FoundationSFID, - EventProjectExternalID: oldProject.ProjectSFID, - EventProjectID: oldProject.ClaGroupID, - EventProjectSFID: oldProject.ProjectSFID, - EventType: claEvents.ProjectServiceCLADisabled, - LfUsername: "easycla system", - UserID: "easycla system", - UserName: "easycla system", - // EventProjectName: "", - // EventProjectSFName: "", - }) - if eventErr != nil { - log.WithFields(f).WithError(eventErr).Warn("problem logging event for disabling CLA service") - // Ok - don't fail for now - } - - return nil -} +//// ProjectServiceEnableCLAServiceHandler handles enabling the CLA Service attribute from the project service +//func (s *service) ProjectServiceEnableCLAServiceHandler(event events.DynamoDBEventRecord) error { +// ctx := utils.NewContext() +// f := logrus.Fields{ +// "functionName": "dynamo_events.projects_cla_groups.ProjectServiceEnableCLAServiceHandler", +// utils.XREQUESTID: ctx.Value(utils.XREQUESTID), +// "eventID": event.EventID, +// "eventName": event.EventName, +// "eventSource": event.EventSource, +// } +// +// log.WithFields(f).Debug("processing request") +// var newProject ProjectClaGroup +// err := unmarshalStreamImage(event.Change.NewImage, &newProject) +// if err != nil { +// log.WithFields(f).WithError(err).Warn("project decoding add event") +// return err +// } +// +// f["projectSFID"] = newProject.ProjectSFID +// f["claGroupID"] = newProject.ClaGroupID +// f["foundationSFID"] = newProject.FoundationSFID +// +// psc := v2ProjectService.GetClient() +// log.WithFields(f).Debug("looking up project by SFID...") +// projectDetails, prjerr := psc.GetProject(newProject.ProjectSFID) +// if prjerr != nil { +// log.WithError(err).Warnf("unable to get project details from SFID: %s", newProject.ProjectSFID) +// } +// projectName := newProject.ProjectSFID +// if projectDetails != nil { +// projectName = projectDetails.Name +// f["projectName"] = projectName +// } +// +// start, _ := utils.CurrentTime() +// log.WithFields(f).Debugf("enabling CLA service for project %s with ID: %s", projectName, newProject.ProjectSFID) +// err = psc.EnableCLA(newProject.ProjectSFID) +// if err != nil { +// log.WithFields(f).WithError(err).Warn("enabling CLA service failed") +// return err +// } +// finish, _ := utils.CurrentTime() +// log.WithFields(f).Debugf("enabled CLA service for project %s with ID: %s", projectName, newProject.ProjectSFID) +// log.WithFields(f).Debugf("enabling CLA service completed - took: %s", finish.Sub(start).String()) +// +// // Log the event +// eventErr := s.eventsRepo.CreateEvent(&models.Event{ +// ContainsPII: false, +// EventData: fmt.Sprintf("enabled CLA service for project: %s with ID: %s", projectName, newProject.ProjectSFID), +// EventFoundationSFID: newProject.FoundationSFID, +// EventProjectExternalID: newProject.ProjectSFID, +// EventProjectID: newProject.ClaGroupID, +// EventProjectName: projectName, +// EventProjectSFID: newProject.ProjectSFID, +// EventProjectSFName: projectName, +// EventSummary: fmt.Sprintf("enabled CLA service for project: %s", projectName), +// EventType: claEvents.ProjectServiceCLAEnabled, +// LfUsername: "easycla system", +// UserID: "easycla system", +// UserName: "easycla system", +// }) +// if eventErr != nil { +// log.WithFields(f).WithError(eventErr).Warn("problem logging event for enabling CLA service") +// // Ok - don't fail for now +// } +// +// return nil +//} +// +//// ProjectServiceDisableCLAServiceHandler handles disabling/removing the CLA Service attribute from the project service +//func (s *service) ProjectServiceDisableCLAServiceHandler(event events.DynamoDBEventRecord) error { +// ctx := utils.NewContext() +// f := logrus.Fields{ +// "functionName": "dynamo_events.projects_cla_groups.ProjectServiceDisableCLAServiceHandler", +// utils.XREQUESTID: ctx.Value(utils.XREQUESTID), +// "eventID": event.EventID, +// "eventName": event.EventName, +// "eventSource": event.EventSource, +// } +// +// log.WithFields(f).Debug("processing request") +// var oldProject ProjectClaGroup +// err := unmarshalStreamImage(event.Change.OldImage, &oldProject) +// if err != nil { +// log.WithFields(f).WithError(err).Warn("problem unmarshalling stream image") +// return err +// } +// +// // Add more fields for the logger +// f["ProjectSFID"] = oldProject.ProjectSFID +// f["ClaGroupID"] = oldProject.ClaGroupID +// f["FoundationSFID"] = oldProject.FoundationSFID +// +// psc := v2ProjectService.GetClient() +// log.WithFields(f).Debug("looking up project by SFID...") +// projectDetails, prjerr := psc.GetProject(oldProject.ProjectSFID) +// if prjerr != nil { +// log.WithError(err).Warnf("unable to get project details from SFID: %s", oldProject.ProjectSFID) +// } +// projectName := oldProject.ProjectSFID +// if projectDetails != nil { +// projectName = projectDetails.Name +// f["projectName"] = projectName +// } +// +// // Gathering metrics - grab the time before the API call +// before, _ := utils.CurrentTime() +// log.WithFields(f).Debugf("disabling CLA service for project %s with ID: %s", projectName, oldProject.ProjectSFID) +// err = psc.DisableCLA(oldProject.ProjectSFID) +// if err != nil { +// log.WithFields(f).WithError(err).Warn("disabling CLA service failed") +// return err +// } +// log.WithFields(f).Debugf("disabled CLA service for project %s with ID: %s", projectName, oldProject.ProjectSFID) +// log.WithFields(f).Debugf("disabling CLA service completed - took %s", time.Since(before).String()) +// +// // Log the event +// eventErr := s.eventsRepo.CreateEvent(&models.Event{ +// ContainsPII: false, +// EventData: fmt.Sprintf("disabled CLA service for project: %s with ID: %s", projectName, oldProject.ProjectSFID), +// EventFoundationSFID: oldProject.FoundationSFID, +// EventProjectExternalID: oldProject.ProjectSFID, +// EventProjectID: oldProject.ClaGroupID, +// EventProjectName: projectName, +// EventProjectSFID: oldProject.ProjectSFID, +// EventSummary: fmt.Sprintf("disabled CLA service for project: %s", projectName), +// EventType: claEvents.ProjectServiceCLADisabled, +// LfUsername: "easycla system", +// UserID: "easycla system", +// UserName: "easycla system", +// }) +// if eventErr != nil { +// log.WithFields(f).WithError(eventErr).Warn("problem logging event for disabling CLA service") +// // Ok - don't fail for now +// } +// +// return nil +//} func (s *service) ProjectUnenrolledDisableRepositoryHandler(event events.DynamoDBEventRecord) error { ctx := utils.NewContext() f := logrus.Fields{ - "functionName": "ProjectUnenrolledDisableRepositoryHandler", + "functionName": "dynamo_events.projects_cla_groups.ProjectUnenrolledDisableRepositoryHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "eventID": event.EventID, "eventName": event.EventName, @@ -190,8 +209,8 @@ func (s *service) ProjectUnenrolledDisableRepositoryHandler(event events.DynamoD // For each GitHub repository... for _, gitHubRepo := range gitHubRepos.List { - log.WithFields(f).Debugf("disabling github repository: %s with id: %s for project with sfid: %s", - gitHubRepo.RepositoryName, gitHubRepo.RepositoryID, gitHubRepo.ProjectSFID) + log.WithFields(f).Debugf("disabling github repository: %s with id: %s for project with sfid: %s for CLA Group: %s", + gitHubRepo.RepositoryName, gitHubRepo.RepositoryID, gitHubRepo.RepositoryProjectSfid, gitHubRepo.RepositoryClaGroupID) disableErr := s.repositoryService.DisableRepository(ctx, gitHubRepo.RepositoryID) if disableErr != nil { log.WithFields(f).WithError(disableErr).Warnf("problem disabling github repository: %s with id: %s", gitHubRepo.RepositoryName, gitHubRepo.RepositoryID) @@ -231,11 +250,13 @@ func (s *service) ProjectUnenrolledDisableRepositoryHandler(event events.DynamoD // AddCLAPermissions handles adding CLA permissions func (s *service) AddCLAPermissions(event events.DynamoDBEventRecord) error { + ctx := utils.NewContext() f := logrus.Fields{ - "functionName": "AddCLAPermissions", - "eventID": event.EventID, - "eventName": event.EventName, - "eventSource": event.EventSource, + "functionName": "dynamo_events.projects_cla_groups.AddCLAPermissions", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "eventID": event.EventID, + "eventName": event.EventName, + "eventSource": event.EventSource, } log.WithFields(f).Debug("processing event") @@ -252,14 +273,14 @@ func (s *service) AddCLAPermissions(event events.DynamoDBEventRecord) error { f["FoundationSFID"] = newProject.FoundationSFID // Add any relevant CLA Manager permissions for this CLA Group/Project SFID - permErr := s.addCLAManagerPermissions(newProject.ClaGroupID, newProject.ProjectSFID) + permErr := s.addCLAManagerPermissions(ctx, newProject.ClaGroupID, newProject.ProjectSFID) if permErr != nil { log.WithFields(f).WithError(permErr).Warn("problem adding CLA Manager permissions for projectSFID") // Ok - don't fail for now } // Add any relevant CLA Manager Designee permissions for this CLA Group/Project SFID - permErr = s.addCLAManagerDesigneePermissions(newProject.ClaGroupID, newProject.FoundationSFID, newProject.ProjectSFID) + permErr = s.addCLAManagerDesigneePermissions(ctx, newProject.ClaGroupID, newProject.FoundationSFID, newProject.ProjectSFID) if permErr != nil { log.WithFields(f).WithError(permErr).Warn("problem adding CLA Manager Designee permissions for projectSFID") // Ok - don't fail for now @@ -270,11 +291,13 @@ func (s *service) AddCLAPermissions(event events.DynamoDBEventRecord) error { // RemoveCLAPermissions handles removing existing CLA permissions func (s *service) RemoveCLAPermissions(event events.DynamoDBEventRecord) error { + ctx := utils.NewContext() f := logrus.Fields{ - "functionName": "RemoveCLAPermissions", - "eventID": event.EventID, - "eventName": event.EventName, - "eventSource": event.EventSource, + "functionName": "dynamo_events.projects_cla_groups.RemoveCLAPermissions", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "eventID": event.EventID, + "eventName": event.EventName, + "eventSource": event.EventSource, } log.WithFields(f).Debug("processing event") @@ -291,7 +314,7 @@ func (s *service) RemoveCLAPermissions(event events.DynamoDBEventRecord) error { f["FoundationSFID"] = oldProject.FoundationSFID // Remove any CLA related permissions - permErr := s.removeCLAPermissions(oldProject.ProjectSFID) + permErr := s.removeCLAPermissions(ctx, oldProject.ProjectSFID) if permErr != nil { log.WithFields(f).WithError(permErr).Warn("problem removing CLA permissions for projectSFID") // Ok - don't fail for now @@ -300,12 +323,25 @@ func (s *service) RemoveCLAPermissions(event events.DynamoDBEventRecord) error { return nil } -func (s *service) addCLAManagerDesigneePermissions(claGroupID, foundationSFID, projectSFID string) error { - ctx := utils.NewContext() +func (s *service) addCLAManagerDesigneePermissions(ctx context.Context, claGroupID, foundationSFID, projectSFID string) error { f := logrus.Fields{ - "functionName": "addCLAManagerDesigneePermissions", - "claGroupID": claGroupID, - "projectSFID": projectSFID, + "functionName": "dynamo_events.projects_cla_groups.addCLAManagerDesigneePermissions", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupID, + "projectSFID": projectSFID, + } + + // Lookup the project name + log.WithFields(f).Debugf("looking up project by SFID: %s", projectSFID) + psc := v2ProjectService.GetClient() + projectModel, err := psc.GetProject(projectSFID) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to lookup project record by projectSFID") + } + projectName := "" + if projectModel != nil { + projectName = projectModel.Name + f["projectName"] = projectName } //handle userscopes per project(users with Designee role) @@ -342,7 +378,7 @@ func (s *service) addCLAManagerDesigneePermissions(claGroupID, foundationSFID, p } else { // Signed at Project level Use case - pcgs, err := s.projectsClaGroupRepo.GetProjectsIdsForClaGroup(claGroupID) + pcgs, err := s.projectsClaGroupRepo.GetProjectsIdsForClaGroup(ctx, claGroupID) if err != nil { log.WithFields(f).WithError(err).Warnf("problem getting project cla Groups for claGroupID: %s", claGroupID) return err @@ -377,12 +413,50 @@ func (s *service) addCLAManagerDesigneePermissions(claGroupID, foundationSFID, p orgID := strings.Split(userScope.ObjectID, "|")[1] email := userScope.Email + // Lookup the organization name + log.WithFields(f).Debugf("looking up organization by SFID: %s", orgID) + orgModel, orgLookupErr := orgClient.GetOrganization(ctx, orgID) + if orgLookupErr != nil { + log.WithFields(f).WithError(orgLookupErr).Warnf("unable to lookup organization record by organziation SFID: %s", orgID) + } + orgName := "" + if orgModel != nil { + orgName = orgModel.Name + log.WithFields(f).Debugf("found organization by SFID: %s - Name: %s", orgID, orgName) + } + + log.WithFields(f).Debugf("assiging role: %s to user %s with email %s for project: %s, company: %s...", + utils.CLAManagerRole, userScope.Username, email, projectSFID, orgID) roleErr := orgClient.CreateOrgUserRoleOrgScopeProjectOrg(ctx, email, projectSFID, orgID, claManagerDesigneeRoleID) if roleErr != nil { log.WithFields(f).WithError(roleErr).Warnf("%s, role assignment for user %s failed for this project: %s, company: %s ", utils.CLADesigneeRole, email, projectSFID, orgID) return } + + msgSummary := fmt.Sprintf("assigned role: %s to user %s with email %s for project: %s, company: %s", + utils.CLAManagerRole, userScope.Username, email, projectName, orgName) + msg := fmt.Sprintf("assigned role: %s to user %s with email %s for project: %s with SFID: %s, company: %s with SFID: %s", + utils.CLAManagerRole, userScope.Username, email, projectName, projectSFID, orgName, orgID) + log.WithFields(f).Debug(msg) + // Log the event + eventErr := s.eventsRepo.CreateEvent(&models.Event{ + ContainsPII: false, + EventCompanySFID: orgID, + EventData: msg, + EventProjectSFID: projectSFID, + EventProjectID: claGroupID, + EventProjectName: projectName, + EventCompanyName: orgName, + EventSummary: msgSummary, + EventType: claEvents.AssignUserRoleScopeType, + LfUsername: "easycla system", + UserID: "easycla system", + UserName: "easycla system", + }) + if eventErr != nil { + log.WithFields(f).WithError(eventErr).Warnf("unable to create event log entry for %s with msg: %s", claEvents.AssignUserRoleScopeType, msg) + } }(userScope) } @@ -395,20 +469,33 @@ func (s *service) addCLAManagerDesigneePermissions(claGroupID, foundationSFID, p } // addCLAManagerPermissions handles adding the CLA Manager permissions for the specified SF project -func (s *service) addCLAManagerPermissions(claGroupID, projectSFID string) error { - ctx := utils.NewContext() +func (s *service) addCLAManagerPermissions(ctx context.Context, claGroupID, projectSFID string) error { f := logrus.Fields{ - "functionName": "addCLAManagerPermissions", - "projectSFID": projectSFID, - "claGroupID": claGroupID, + "functionName": "dynamo_events.projects_cla_groups.addCLAManagerPermissions", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + "claGroupID": claGroupID, } log.WithFields(f).Debug("adding CLA Manager permissions...") + // Lookup the project name + log.WithFields(f).Debugf("looking up project by SFID: %s", projectSFID) + psc := v2ProjectService.GetClient() + projectModel, err := psc.GetProject(projectSFID) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to lookup project record by projectSFID") + } + projectName := "" + if projectModel != nil { + projectName = projectModel.Name + f["projectName"] = projectName + } + sigModels, err := s.signatureRepo.GetProjectSignatures(ctx, signatures.GetProjectSignaturesParams{ ClaType: aws.String(utils.ClaTypeCCLA), PageSize: aws.Int64(1000), ProjectID: claGroupID, - }, 1000) + }) if err != nil { log.WithFields(f).WithError(err).Warnf("problem querying CCLA signatures for CLA Group - skipping %s role review/assignment for this project", utils.CLAManagerRole) return err @@ -434,7 +521,7 @@ func (s *service) addCLAManagerPermissions(claGroupID, projectSFID string) error // Make sure we can load the company and grab the SFID sig := sig - companyInternalID := sig.SignatureReferenceID.String() + companyInternalID := sig.SignatureReferenceID log.WithFields(f).Debugf("locating company by internal ID: %s", companyInternalID) companyModel, err := s.companyRepo.GetCompany(ctx, companyInternalID) if err != nil { @@ -473,7 +560,7 @@ func (s *service) addCLAManagerPermissions(claGroupID, projectSFID string) error signatureUserModel.LfUsername, utils.CLAManagerRole) return } - if userModel == nil || userModel.ID == "" || userModel.Email == nil { + if userModel == nil || userModel.ID == "" || userClient.GetPrimaryEmail(userModel) == "" { log.WithFields(f).Warnf("unable to lookup user %s - user object is empty or missing either the ID or email - skipping %s role review/assigment for project: %s, company: %s", signatureUserModel.LfUsername, utils.CLAManagerRole, projectSFID, companySFID) return @@ -489,20 +576,44 @@ func (s *service) addCLAManagerPermissions(claGroupID, projectSFID string) error // Does the user already have the cla-manager role? if hasRole { - log.WithFields(f).Debugf("user %s/%s already has role %s for the project %s and organization %s", + log.WithFields(f).Debugf("user %s/%s already has role %s for the project %s and organization %s - skipping assignment", signatureUserModel.LfUsername, userModel.ID, utils.CLAManagerRole, projectSFID, companySFID) // Nothing to do here - move along... return } // Finally....assign the role to this user - roleErr := orgClient.CreateOrgUserRoleOrgScopeProjectOrg(ctx, aws.StringValue(userModel.Email), projectSFID, companySFID, claManagerRoleID) + log.WithFields(f).Debugf("assiging role: %s to user %s/%s/%s for project: %s, company: %s...", + utils.CLAManagerRole, signatureUserModel.LfUsername, userModel.ID, userClient.GetPrimaryEmail(userModel), projectSFID, companySFID) + roleErr := orgClient.CreateOrgUserRoleOrgScopeProjectOrg(ctx, userClient.GetPrimaryEmail(userModel), projectSFID, companySFID, claManagerRoleID) if roleErr != nil { log.WithFields(f).WithError(roleErr).Warnf("%s, role assignment for user user %s/%s/%s failed for this project: %s, company: %s", - utils.CLAManagerRole, signatureUserModel.LfUsername, userModel.ID, *userModel.Email, projectSFID, companySFID) + utils.CLAManagerRole, signatureUserModel.LfUsername, userModel.ID, userClient.GetPrimaryEmail(userModel), projectSFID, companySFID) return } - + msg := fmt.Sprintf("assigned role: %s to user %s/%s/%s for project: %s with SFID:%s, company: %s with SFID: %s", + utils.CLAManagerRole, signatureUserModel.LfUsername, userModel.ID, userClient.GetPrimaryEmail(userModel), projectName, projectSFID, companyModel.CompanyName, companySFID) + msgSummary := fmt.Sprintf("assigned role: %s to user %s/%s/%s for project: %s, company: %s", + utils.CLAManagerRole, signatureUserModel.LfUsername, userModel.ID, userClient.GetPrimaryEmail(userModel), projectName, companyModel.CompanyName) + log.WithFields(f).Debug(msg) + // Log the event + eventErr := s.eventsRepo.CreateEvent(&models.Event{ + ContainsPII: false, + EventCompanyName: companyModel.CompanyName, + EventCompanySFID: companySFID, + EventData: msg, + EventProjectID: claGroupID, + EventProjectName: projectName, + EventProjectSFID: projectSFID, + EventSummary: msgSummary, + EventType: claEvents.AssignUserRoleScopeType, + LfUsername: "easycla system", + UserID: "easycla system", + UserName: "easycla system", + }) + if eventErr != nil { + log.WithFields(f).WithError(eventErr).Warnf("unable to create event log entry for %s with msg: %s", claEvents.AssignUserRoleScopeType, msg) + } }(signatureUserModel) } @@ -515,37 +626,118 @@ func (s *service) addCLAManagerPermissions(claGroupID, projectSFID string) error } // removeCLAPermissions handles removing CLA Group (projects table) permissions for the specified project -func (s *service) removeCLAPermissions(projectSFID string) error { +func (s *service) removeCLAPermissions(ctx context.Context, projectSFID string) error { f := logrus.Fields{ - "functionName": "removeCLAPermissions", - "projectSFID": projectSFID, + "functionName": "dynamo_events.projects_cla_groups.removeCLAPermissions", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, } log.WithFields(f).Debug("removing CLA permissions...") + // Lookup the project name + log.WithFields(f).Debugf("looking up project by SFID: %s", projectSFID) + psc := v2ProjectService.GetClient() + projectModel, err := psc.GetProject(projectSFID) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to lookup project record by projectSFID") + } + projectName := "" + if projectModel != nil { + projectName = projectModel.Name + f["projectName"] = projectName + } + client := acsService.GetClient() - err := client.RemoveCLAUserRolesByProject(projectSFID, []string{utils.CLAManagerRole, utils.CLADesigneeRole, utils.CLASignatoryRole}) + roleNames := []string{utils.CLAManagerRole, utils.CLADesigneeRole, utils.CLASignatoryRole} + + log.WithFields(f).Debugf("removing roles: %s for all users for project: %s", strings.Join(roleNames, ","), projectSFID) + err = client.RemoveCLAUserRolesByProject(projectSFID, roleNames) if err != nil { log.WithFields(f).WithError(err).Warn("problem removing CLA user roles by projectSFID") } + msg := fmt.Sprintf("removed roles: %s for all users for project: %s", strings.Join(roleNames, ","), projectSFID) + log.WithFields(f).Debug(msg) + + // Log the event + eventErr := s.eventsRepo.CreateEvent(&models.Event{ + ContainsPII: false, + EventData: msg, + EventProjectName: projectName, + EventProjectSFID: projectSFID, + EventSummary: msg, + EventType: claEvents.RemoveUserRoleScopeType, + LfUsername: "easycla system", + UserID: "easycla system", + UserName: "easycla system", + }) + if eventErr != nil { + log.WithFields(f).WithError(eventErr).Warnf("unable to create event log entry for %s with msg: %s", claEvents.RemoveUserRoleScopeType, msg) + } return err } // removeCLAPermissionsByProjectOrganizationRole handles removal of the specified role for the given SF Project and SF Organization -func (s *service) removeCLAPermissionsByProjectOrganizationRole(projectSFID, organizationSFID string, roleNames []string) error { +func (s *service) removeCLAPermissionsByProjectOrganizationRole(ctx context.Context, projectSFID, organizationSFID string, roleNames []string) error { f := logrus.Fields{ - "functionName": "removeCLAPermissionsByProjectOrganizationRole", + "functionName": "dynamo_events.projects_cla_groups.removeCLAPermissionsByProjectOrganizationRole", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": projectSFID, "organizationSFID": organizationSFID, "roleNames": strings.Join(roleNames, ","), } - log.WithFields(f).Debug("removing CLA permissions...") + // Lookup the project name + log.WithFields(f).Debugf("looking up project by SFID: %s", projectSFID) + psc := v2ProjectService.GetClient() + projectModel, err := psc.GetProject(projectSFID) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to lookup project record by projectSFID") + } + projectName := "" + if projectModel != nil { + projectName = projectModel.Name + f["projectName"] = projectName + } + + // Lookup the organization name + log.WithFields(f).Debugf("looking up organization by SFID: %s", organizationSFID) + orgClient := organizationService.GetClient() + orgModel, orgLookupErr := orgClient.GetOrganization(ctx, organizationSFID) + if orgLookupErr != nil { + log.WithFields(f).WithError(orgLookupErr).Warnf("unable to lookup organization record by organziation SFID: %s", organizationSFID) + } + orgName := "" + if orgModel != nil { + orgName = orgModel.Name + log.WithFields(f).Debugf("found organization by SFID: %s - Name: %s", organizationSFID, orgName) + } + + log.WithFields(f).Debugf("removing roles: %s for all users for project: %s, companay: %s", strings.Join(roleNames, ","), projectSFID, organizationSFID) client := acsService.GetClient() - err := client.RemoveCLAUserRolesByProjectOrganization(projectSFID, organizationSFID, roleNames) + err = client.RemoveCLAUserRolesByProjectOrganization(projectSFID, organizationSFID, roleNames) if err != nil { log.WithFields(f).WithError(err).Warn("problem removing CLA user roles by projectSFID and organizationSFID") } + msg := fmt.Sprintf("removed roles: %s for all users for project: %s, companay: %s", strings.Join(roleNames, ","), projectSFID, organizationSFID) + log.WithFields(f).Debug(msg) + + // Log the event + eventErr := s.eventsRepo.CreateEvent(&models.Event{ + ContainsPII: false, + EventCompanySFID: organizationSFID, + EventData: msg, + EventProjectName: projectName, + EventProjectSFID: projectSFID, + EventSummary: msg, + EventType: claEvents.RemoveUserRoleScopeType, + LfUsername: "easycla system", + UserID: "easycla system", + UserName: "easycla system", + }) + if eventErr != nil { + log.WithFields(f).WithError(eventErr).Warnf("unable to create event log entry for %s with msg: %s", claEvents.RemoveUserRoleScopeType, msg) + } return err } diff --git a/cla-backend-go/v2/dynamo_events/service.go b/cla-backend-go/v2/dynamo_events/service.go index 378a4edc9..cd20293b8 100644 --- a/cla-backend-go/v2/dynamo_events/service.go +++ b/cla-backend-go/v2/dynamo_events/service.go @@ -11,6 +11,14 @@ import ( "strings" "sync" + "github.com/communitybridge/easycla/cla-backend-go/project/repository" + service2 "github.com/communitybridge/easycla/cla-backend-go/project/service" + + v2Repositories "github.com/communitybridge/easycla/cla-backend-go/v2/repositories" + + gitlab_api "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" + "github.com/communitybridge/easycla/cla-backend-go/v2/gitlab_organizations" + "github.com/communitybridge/easycla/cla-backend-go/gerrits" "github.com/communitybridge/easycla/cla-backend-go/repositories" @@ -21,7 +29,6 @@ import ( "github.com/communitybridge/easycla/cla-backend-go/cla_manager" claevent "github.com/communitybridge/easycla/cla-backend-go/events" - "github.com/communitybridge/easycla/cla-backend-go/project" "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" "github.com/communitybridge/easycla/cla-backend-go/company" @@ -56,14 +63,18 @@ type service struct { companyService v2Company.Service projectsClaGroupRepo projects_cla_groups.Repository eventsRepo claevent.Repository - projectRepo project.ProjectRepository - projectService project.Service - githubOrgService github_organizations.Service + gitLabOrgRepo gitlab_organizations.RepositoryInterface + gitLabOrgService gitlab_organizations.ServiceInterface + v2Repository v2Repositories.RepositoryInterface + projectRepo repository.ProjectRepository + projectService service2.Service + githubOrgService github_organizations.ServiceInterface repositoryService repositories.Service gerritService gerrits.Service autoEnableService *autoEnableServiceProvider claManagerRequestsRepo cla_manager.IRepository approvalListRequestsRepo approval_list.IRepository + gitLabApp *gitlab_api.App } // Service implements DynamoDB stream event handler service @@ -78,19 +89,24 @@ func NewService(stage string, companyService v2Company.Service, pcgRepo projects_cla_groups.Repository, eventsRepo claevent.Repository, - projectRepo project.ProjectRepository, - projService project.Service, - githubOrgService github_organizations.Service, + projectRepo repository.ProjectRepository, + gitLabOrgRepo gitlab_organizations.RepositoryInterface, + v2Repository v2Repositories.RepositoryInterface, + projService service2.Service, + githubOrgService github_organizations.ServiceInterface, repositoryService repositories.Service, gerritService gerrits.Service, claManagerRequestsRepo cla_manager.IRepository, - approvalListRequestsRepo approval_list.IRepository) Service { + approvalListRequestsRepo approval_list.IRepository, + gitLabApp *gitlab_api.App, + gitlabOrgService gitlab_organizations.ServiceInterface) Service { signaturesTable := fmt.Sprintf("cla-%s-signatures", stage) eventsTable := fmt.Sprintf("cla-%s-events", stage) projectsCLAGroupsTable := fmt.Sprintf("cla-%s-projects-cla-groups", stage) githubOrgTableName := fmt.Sprintf("cla-%s-github-orgs", stage) repositoryTableName := fmt.Sprintf("cla-%s-repositories", stage) + gitlabOrgTableName := fmt.Sprintf("cla-%s-gitlab-orgs", stage) // gerritTableName := fmt.Sprintf("cla-%s-gerrit-instances", stage) claGroupsTable := fmt.Sprintf("cla-%s-projects", stage) @@ -102,6 +118,8 @@ func NewService(stage string, projectsClaGroupRepo: pcgRepo, eventsRepo: eventsRepo, projectRepo: projectRepo, + gitLabOrgRepo: gitLabOrgRepo, + v2Repository: v2Repository, projectService: projService, githubOrgService: githubOrgService, repositoryService: repositoryService, @@ -109,6 +127,8 @@ func NewService(stage string, autoEnableService: &autoEnableServiceProvider{repositoryService: repositoryService}, claManagerRequestsRepo: claManagerRequestsRepo, approvalListRequestsRepo: approvalListRequestsRepo, + gitLabApp: gitLabApp, + gitLabOrgService: gitlabOrgService, } s.registerCallback(signaturesTable, Modify, s.SignatureSignedEvent) @@ -122,8 +142,9 @@ func NewService(stage string, s.registerCallback(eventsTable, Insert, s.EventAddedEvent) // Enable or Disable the CLA Service Enabled/Disabled flag/attribute in the platform Project Service - s.registerCallback(projectsCLAGroupsTable, Insert, s.ProjectServiceEnableCLAServiceHandler) - s.registerCallback(projectsCLAGroupsTable, Remove, s.ProjectServiceDisableCLAServiceHandler) + // These are called by the API via the service layer - includes the user who did it + //s.registerCallback(projectsCLAGroupsTable, Insert, s.ProjectServiceEnableCLAServiceHandler) + //s.registerCallback(projectsCLAGroupsTable, Remove, s.ProjectServiceDisableCLAServiceHandler) s.registerCallback(projectsCLAGroupsTable, Remove, s.ProjectUnenrolledDisableRepositoryHandler) // Add or Remove any CLA Permissions for the specified project @@ -139,6 +160,13 @@ func NewService(stage string, s.registerCallback(repositoryTableName, Modify, s.GithubRepoModifyAddEvent) s.registerCallback(repositoryTableName, Remove, s.GithubRepoModifyAddEvent) + s.registerCallback(repositoryTableName, Insert, s.GitLabRepoAddedWebhookEventHandler) + s.registerCallback(repositoryTableName, Modify, s.GitlabRepoModifiedWebhookEventHandler) + s.registerCallback(repositoryTableName, Remove, s.GitLabRepoRemovedWebhookEventHandler) + + // gitlab org updates handled like branch protection and etc. + s.registerCallback(gitlabOrgTableName, Modify, s.GitLabOrgUpdatedEvent) + // Check and enable/disable the branch protection when a project s.registerCallback(repositoryTableName, Insert, s.EnableBranchProtectionServiceHandler) s.registerCallback(repositoryTableName, Remove, s.DisableBranchProtectionServiceHandler) @@ -167,9 +195,7 @@ func (s *service) ProcessEvents(dynamoDBEvents events.DynamoDBEvent) { // Dumping the event is super verbose // "event": event, } - // Generates a ton of output - // b, _ := json.Marshal(events) // nolint - //fields["events_data"] = string(b) + log.WithFields(fields).Debug("processing event record") key := fmt.Sprintf("%s:%s", tableName, event.EventName) diff --git a/cla-backend-go/v2/dynamo_events/signatures.go b/cla-backend-go/v2/dynamo_events/signatures.go index dc2bbd7b1..c40a8e263 100644 --- a/cla-backend-go/v2/dynamo_events/signatures.go +++ b/cla-backend-go/v2/dynamo_events/signatures.go @@ -31,7 +31,7 @@ const ( DeleteCLAManager = "delete" ) -//ErrNoExternalID when company does not have externalID +// ErrNoExternalID when company does not have externalID var ErrNoExternalID = errors.New("company External ID does not exist") // Signature database model @@ -186,7 +186,7 @@ func (s *service) SignatureSignedEvent(event events.DynamoDBEventRecord) error { // Load the list of SF projects associated with this CLA Group log.WithFields(f).Debugf("querying SF projects for CLA Group: %s", newSignature.SignatureProjectID) - projectCLAGroups, err := s.projectsClaGroupRepo.GetProjectsIdsForClaGroup(newSignature.SignatureProjectID) + projectCLAGroups, err := s.projectsClaGroupRepo.GetProjectsIdsForClaGroup(ctx, newSignature.SignatureProjectID) log.WithFields(f).Debugf("found %d SF projects for CLA Group: %s", len(projectCLAGroups), newSignature.SignatureProjectID) // Only proceed if we have one or more SF projects - otherwise, we can't assign and cleanup/adjust roles @@ -219,7 +219,7 @@ func (s *service) SignatureSignedEvent(event events.DynamoDBEventRecord) error { if signedAtFoundation { log.WithFields(f).Debugf("removing existing %s role for project: '%s' (%s) and company: '%s' (%s)", utils.CLADesigneeRole, projectCLAGroups[0].ProjectName, foundationSFID, companyModel.CompanyName, companyModel.CompanyExternalID) - err = s.removeCLAPermissionsByProjectOrganizationRole(foundationSFID, companyModel.CompanyExternalID, []string{utils.CLADesigneeRole}) + err = s.removeCLAPermissionsByProjectOrganizationRole(ctx, foundationSFID, companyModel.CompanyExternalID, []string{utils.CLADesigneeRole}) if err != nil { log.WithFields(f).Warnf("failed to remove %s roles for project: '%s' (%s) and company: '%s' (%s), error: %+v", utils.CLADesigneeRole, projectCLAGroups[0].ProjectName, foundationSFID, companyModel.CompanyName, companyModel.CompanyExternalID, err) @@ -227,14 +227,15 @@ func (s *service) SignatureSignedEvent(event events.DynamoDBEventRecord) error { } } else { for _, projectCLAGroup := range projectCLAGroups { + pcg := projectCLAGroup // make a copy of the loop variable to use in the closure, avoids the loopclosure: loop variable projectCLAGroup captured by func literal lint error eg.Go(func() error { // Remove any roles that were previously assigned for cla-manager-designee log.WithFields(f).Debugf("removing existing %s role for project: '%s' (%s) and company: '%s' (%s)", - utils.CLADesigneeRole, projectCLAGroup.ProjectName, projectCLAGroup.ProjectSFID, companyModel.CompanyName, companyModel.CompanyExternalID) - err = s.removeCLAPermissionsByProjectOrganizationRole(projectCLAGroup.ProjectSFID, companyModel.CompanyExternalID, []string{utils.CLADesigneeRole}) + utils.CLADesigneeRole, pcg.ProjectName, pcg.ProjectSFID, companyModel.CompanyName, companyModel.CompanyExternalID) + err = s.removeCLAPermissionsByProjectOrganizationRole(ctx, pcg.ProjectSFID, companyModel.CompanyExternalID, []string{utils.CLADesigneeRole}) if err != nil { log.WithFields(f).Warnf("failed to remove %s roles for project: '%s' (%s) and company: '%s' (%s), error: %+v", - utils.CLADesigneeRole, projectCLAGroup.ProjectName, projectCLAGroup.ProjectSFID, companyModel.CompanyName, companyModel.CompanyExternalID, err) + utils.CLADesigneeRole, pcg.ProjectName, pcg.ProjectSFID, companyModel.CompanyName, companyModel.CompanyExternalID, err) return err } @@ -327,10 +328,8 @@ func (s *service) SignatureAddUsersDetails(event events.DynamoDBEventRecord) err // signature function should be invoked when signature ACL is updated func (s *service) UpdateCLAPermissions(event events.DynamoDBEventRecord) error { - ctx := utils.NewContext() f := logrus.Fields{ - "functionName": "UpdateCLAPermissions", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "functionName": "v2.dynamo_events.UpdateCLAPermissions", } // Decode the pre-update and post-update signature record details @@ -421,7 +420,7 @@ func (s *service) assignContributor(ctx context.Context, newSignature Signature, } // Load the list of SF projects associated with this CLA Group log.WithFields(f).Debugf("querying SF projects for CLA Group: %s", newSignature.SignatureProjectID) - projectCLAGroups, err := s.projectsClaGroupRepo.GetProjectsIdsForClaGroup(newSignature.SignatureProjectID) + projectCLAGroups, err := s.projectsClaGroupRepo.GetProjectsIdsForClaGroup(ctx, newSignature.SignatureProjectID) log.WithFields(f).Debugf("found %d SF projects for CLA Group: %s", len(projectCLAGroups), newSignature.SignatureProjectID) if err != nil { @@ -483,7 +482,7 @@ func (s *service) updateCLAManagerPermissions(signature Signature, managers []st return orgErr } - projectCLAGroups, pcgErr := s.projectsClaGroupRepo.GetProjectsIdsForClaGroup(signature.SignatureProjectID) + projectCLAGroups, pcgErr := s.projectsClaGroupRepo.GetProjectsIdsForClaGroup(ctx, signature.SignatureProjectID) if pcgErr != nil { log.WithFields(f).WithError(pcgErr).Warnf("unable to get project mappings for claGroupID: %s ", signature.SignatureProjectID) return pcgErr diff --git a/cla-backend-go/v2/events/converters.go b/cla-backend-go/v2/events/converters.go index 4f0528de8..868026574 100644 --- a/cla-backend-go/v2/events/converters.go +++ b/cla-backend-go/v2/events/converters.go @@ -4,7 +4,7 @@ package events import ( - v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/models" + v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" "github.com/jinzhu/copier" ) diff --git a/cla-backend-go/v2/events/csvResponse.go b/cla-backend-go/v2/events/csvResponse.go index e937e68ea..12641fab1 100644 --- a/cla-backend-go/v2/events/csvResponse.go +++ b/cla-backend-go/v2/events/csvResponse.go @@ -10,7 +10,7 @@ import ( "github.com/communitybridge/easycla/cla-backend-go/utils" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" diff --git a/cla-backend-go/v2/events/handlers.go b/cla-backend-go/v2/events/handlers.go index e5625b761..9514d6552 100644 --- a/cla-backend-go/v2/events/handlers.go +++ b/cla-backend-go/v2/events/handlers.go @@ -21,17 +21,17 @@ import ( "github.com/LF-Engineering/lfx-kit/auth" v1Company "github.com/communitybridge/easycla/cla-backend-go/company" v1Events "github.com/communitybridge/easycla/cla-backend-go/events" - v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/models" + 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/gen/v2/restapi/operations" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations/events" + v1ProjectService "github.com/communitybridge/easycla/cla-backend-go/project/service" "github.com/communitybridge/easycla/cla-backend-go/utils" - v2ProjectService "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" "github.com/go-openapi/runtime/middleware" ) // Configure setups handlers on api with service -func Configure(api *operations.EasyclaAPI, service v1Events.Service, v1CompanyRepo v1Company.IRepository, projectsClaGroupsRepo projects_cla_groups.Repository) { // nolint +func Configure(api *operations.EasyclaAPI, service v1Events.Service, v1CompanyRepo v1Company.IRepository, projectsClaGroupsRepo projects_cla_groups.Repository, projectService v1ProjectService.Service) { // nolint api.EventsGetRecentEventsHandler = events.GetRecentEventsHandlerFunc( func(params events.GetRecentEventsParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) @@ -89,7 +89,7 @@ func Configure(api *operations.EasyclaAPI, service v1Events.Service, v1CompanyRe } log.WithFields(f).Debug("checking permission...") - if !utils.IsUserAuthorizedForProjectTree(authUser, params.FoundationSFID, utils.ALLOW_ADMIN_SCOPE) { + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.FoundationSFID, utils.ALLOW_ADMIN_SCOPE) { msg := fmt.Sprintf("user %s does not have access to Get Foundation Events for foundation %s.", authUser.UserName, params.FoundationSFID) log.WithFields(f).Warn(msg) return WriteResponse(http.StatusForbidden, runtime.JSONMime, runtime.JSONProducer(), utils.ErrorResponseForbidden(reqID, msg)) @@ -120,12 +120,13 @@ func Configure(api *operations.EasyclaAPI, service v1Events.Service, v1CompanyRe } log.WithFields(f).Debug("checking permission...") - if !utils.IsUserAuthorizedForProjectTree(authUser, params.FoundationSFID, utils.ALLOW_ADMIN_SCOPE) { + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.FoundationSFID, utils.ALLOW_ADMIN_SCOPE) { msg := fmt.Sprintf("user %s does not have access to Get Foundation Events for foundation %s.", authUser.UserName, params.FoundationSFID) log.WithFields(f).Warn(msg) return events.NewGetRecentEventsForbidden().WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } + log.WithFields(f).Debug("querying foundation events...") result, err := service.GetFoundationEvents(params.FoundationSFID, params.NextKey, params.PageSize, aws.BoolValue(params.ReturnAllEvents), params.SearchTerm) if err != nil { msg := "problem fetching foundation events" @@ -165,7 +166,7 @@ func Configure(api *operations.EasyclaAPI, service v1Events.Service, v1CompanyRe } log.WithFields(f).Debug("checking permission...") - if !utils.IsUserAuthorizedForProjectTree(authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { msg := fmt.Sprintf("user %s does not have access to Get Project Events for foundation %s.", authUser.UserName, params.ProjectSFID) log.WithFields(f).Warn(msg) return WriteResponse(http.StatusForbidden, runtime.JSONMime, runtime.JSONProducer(), &models.ErrorResponse{ @@ -175,7 +176,7 @@ func Configure(api *operations.EasyclaAPI, service v1Events.Service, v1CompanyRe }) } - pm, err := projectsClaGroupsRepo.GetClaGroupIDForProject(params.ProjectSFID) + pm, err := projectsClaGroupsRepo.GetClaGroupIDForProject(ctx, params.ProjectSFID) if err != nil { if err == projects_cla_groups.ErrProjectNotAssociatedWithClaGroup { msg := fmt.Sprintf("no cla group associated with this project: %s", params.ProjectSFID) @@ -217,14 +218,15 @@ func Configure(api *operations.EasyclaAPI, service v1Events.Service, v1CompanyRe } log.WithFields(f).Debug("checking permission...") - if !utils.IsUserAuthorizedForProjectTree(authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { msg := fmt.Sprintf("user %s does not have access to Get Project Events for foundation %s.", authUser.UserName, params.ProjectSFID) log.WithFields(f).Warn(msg) return events.NewGetRecentEventsForbidden().WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } // Lookup the CLA Group associated with this Project SFID... - pm, err := projectsClaGroupsRepo.GetClaGroupIDForProject(params.ProjectSFID) + log.WithFields(f).Debugf("loading CLA Group for projectSFID: %s", params.ProjectSFID) + pm, err := projectsClaGroupsRepo.GetClaGroupIDForProject(ctx, params.ProjectSFID) if err != nil { msg := fmt.Sprintf("problem loading CLA Group from Project SFID:: %s", params.ProjectSFID) log.WithFields(f).Warn(msg) @@ -241,6 +243,7 @@ func Configure(api *operations.EasyclaAPI, service v1Events.Service, v1CompanyRe } // Lookup any events for this CLA Group.... + log.WithFields(f).Debugf("loading CLA Group %s events using ID: %s", pm.ClaGroupName, pm.ClaGroupID) result, err := service.GetClaGroupEvents(pm.ClaGroupID, params.NextKey, params.PageSize, aws.BoolValue(params.ReturnAllEvents), params.SearchTerm) if err != nil { msg := fmt.Sprintf("problem loading events for CLA Group: %s with ID: %s error: %v", pm.ClaGroupName, pm.ClaGroupID, err.Error()) @@ -279,43 +282,38 @@ func Configure(api *operations.EasyclaAPI, service v1Events.Service, v1CompanyRe "authUserName": authUser.UserName, "authUserEmail": authUser.Email, "projectSFID": params.ProjectSFID, - "companySFID": params.CompanySFID, + "companyID": params.CompanyID, } - if !utils.IsUserAuthorizedForOrganization(authUser, params.CompanySFID, utils.ALLOW_ADMIN_SCOPE) { + + v1Company, compErr := v1CompanyRepo.GetCompany(ctx, params.CompanyID) + if compErr != nil { + log.WithFields(f).Warnf("unable to fetch company by ID:%s ", params.CompanyID) + return events.NewGetCompanyProjectEventsBadRequest().WithPayload(errorResponse(reqID, compErr)) + } + + if !utils.IsUserAuthorizedForOrganization(ctx, authUser, v1Company.CompanyExternalID, utils.ALLOW_ADMIN_SCOPE) { return events.NewGetCompanyProjectEventsForbidden().WithPayload(&models.ErrorResponse{ Code: "403", Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to GetCompanyProject Events with Organization scope of %s", - authUser.UserName, params.CompanySFID), + authUser.UserName, v1Company.CompanyExternalID), XRequestID: reqID, }) } var err error - psc := v2ProjectService.GetClient() - projectDetails, err := psc.GetProject(params.ProjectSFID) + + var result *v1Models.EventList + + log.WithFields(f).Debugf("loading CLA Group for projectSFID: %s", params.ProjectSFID) + pm, err := projectsClaGroupsRepo.GetClaGroupIDForProject(ctx, params.ProjectSFID) + if err != nil { - log.WithFields(f).Warnf("problem loading project by SFID: %s", params.ProjectSFID) + log.WithFields(f).Warnf("unable to fetch project cla mapping by ID:%s ", params.ProjectSFID) return events.NewGetCompanyProjectEventsBadRequest().WithPayload(errorResponse(reqID, err)) } - var result *v1Models.EventList - if projectDetails.ProjectType == utils.ProjectTypeProjectGroup { - result, err = service.GetCompanyFoundationEvents(params.CompanySFID, params.ProjectSFID, params.NextKey, params.PageSize, aws.BoolValue(params.ReturnAllEvents)) - } else { - pm, perr := projectsClaGroupsRepo.GetClaGroupIDForProject(params.ProjectSFID) - if perr != nil { - if perr == projects_cla_groups.ErrProjectNotAssociatedWithClaGroup { - // Although the API should view this as a bad request since the project doesn't seem to belong to a - // CLA Group...just return a successful 200 with an empty list to the caller - nothing to see here, move along. - return events.NewGetCompanyProjectEventsOK().WithPayload(&models.EventList{ - Events: []*models.Event{}, - }) - } - log.WithFields(f).WithError(perr).Warnf("problem determining CLA Group for project SFID: %s", params.ProjectSFID) - return events.NewGetCompanyProjectEventsInternalServerError().WithPayload(errorResponse(reqID, perr)) - } - result, err = service.GetCompanyClaGroupEvents(params.CompanySFID, pm.ClaGroupID, params.NextKey, params.PageSize, aws.BoolValue(params.ReturnAllEvents)) - } + result, err = service.GetCompanyClaGroupEvents(pm.ClaGroupID, v1Company.CompanyExternalID, params.NextKey, params.PageSize, params.SearchTerm, aws.BoolValue(params.ReturnAllEvents)) + if err != nil { log.WithFields(f).WithError(err).Warn("problem loading events") return events.NewGetCompanyProjectEventsBadRequest().WithPayload(errorResponse(reqID, err)) diff --git a/cla-backend-go/v2/gerrits/handlers.go b/cla-backend-go/v2/gerrits/handlers.go index 7a2f62d0f..1fb02686a 100644 --- a/cla-backend-go/v2/gerrits/handlers.go +++ b/cla-backend-go/v2/gerrits/handlers.go @@ -8,11 +8,15 @@ import ( "fmt" "strings" + "github.com/sirupsen/logrus" + "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/LF-Engineering/lfx-kit/auth" "github.com/communitybridge/easycla/cla-backend-go/events" - v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/models" + 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/gen/v2/restapi/operations" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations/gerrits" @@ -22,50 +26,64 @@ import ( "github.com/jinzhu/copier" ) +const decodeErrorMsg = "unable to decode response as a v2 model" + type ProjectService interface { //nolint GetCLAGroupByID(ctx context.Context, claGroupID string) (*v1Models.ClaGroup, error) } // Configure the Gerrit api -func Configure(api *operations.EasyclaAPI, v1Service v1Gerrits.Service, projectService ProjectService, eventService events.Service, projectsClaGroupsRepo projects_cla_groups.Repository) { +func Configure(api *operations.EasyclaAPI, v1Service v1Gerrits.Service, projectService ProjectService, eventService events.Service, projectsClaGroupsRepo projects_cla_groups.Repository) { // nolint api.GerritsDeleteGerritHandler = gerrits.DeleteGerritHandlerFunc( func(params gerrits.DeleteGerritParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + f := logrus.Fields{ + "functionName": "v2.gerrits.handlers.GerritsDeleteGerritHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": params.ProjectSFID, + "claGroupID": params.ClaGroupID, + "gerritID": params.GerritID, + "authUserName": authUser.UserName, + "authUserEmail": authUser.Email, + } + log.WithFields(f).Debugf("querying for gerrits using gerrit ID: %s", params.GerritID) gerrit, err := v1Service.GetGerrit(ctx, params.GerritID) if err != nil { + msg := fmt.Sprintf("unable to locate gerrit by ID: %s", params.GerritID) + log.WithFields(f).Warn(msg) if err == v1Gerrits.ErrGerritNotFound { - return gerrits.NewDeleteGerritNotFound().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) + return gerrits.NewDeleteGerritNotFound().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFoundWithError(reqID, msg, err)) } - return gerrits.NewDeleteGerritInternalServerError().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) + return gerrits.NewDeleteGerritInternalServerError().WithXRequestID(reqID).WithPayload(utils.ErrorResponseInternalServerErrorWithError(reqID, msg, err)) } + if gerrit.ProjectSFID != params.ProjectSFID || gerrit.ProjectID != params.ClaGroupID { - return gerrits.NewDeleteGerritBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "400", - Message: "EasyCLA - 403 Bad Request - projectSFID or claGroupID does not match with provided gerrit record", - XRequestID: reqID, - }) + msg := fmt.Sprintf("projectSFID %s or claGroupID %s does not match with provided gerrit record", params.ProjectSFID, params.ClaGroupID) + log.WithFields(f).Warn(msg) + return gerrits.NewDeleteGerritBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequest(reqID, msg)) } + // verify user have access to the project - if !utils.IsUserAuthorizedForProjectTree(authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { - return gerrits.NewDeleteGerritForbidden().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "403", - Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to DeleteGerrit with Project scope of %s", - authUser.UserName, gerrit.ProjectSFID), - XRequestID: reqID, - }) + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to DeleteGerrit with Project scope of %s", + authUser.UserName, gerrit.ProjectSFID) + log.WithFields(f).Warn(msg) + return gerrits.NewDeleteGerritForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } // delete the gerrit err = v1Service.DeleteGerrit(ctx, params.GerritID) if err != nil { - return gerrits.NewDeleteGerritBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) + msg := "unable to delete gerrit instance" + log.WithFields(f).WithError(err).Warn(msg) + return gerrits.NewDeleteGerritForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } // record the event - eventService.LogEvent(&events.LogEventArgs{ + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.GerritRepositoryDeleted, ProjectID: gerrit.ProjectID, LfUsername: authUser.UserName, @@ -84,7 +102,7 @@ func Configure(api *operations.EasyclaAPI, v1Service v1Gerrits.Service, projectS utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) // verify user have access to the project - if !utils.IsUserAuthorizedForProjectTree(authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { return gerrits.NewAddGerritForbidden().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Code: "403", Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to AddGerrit with Project scope of %s", @@ -92,7 +110,7 @@ func Configure(api *operations.EasyclaAPI, v1Service v1Gerrits.Service, projectS XRequestID: reqID, }) } - ok, err := projectsClaGroupsRepo.IsAssociated(params.ProjectSFID, params.ClaGroupID) + ok, err := projectsClaGroupsRepo.IsAssociated(ctx, params.ProjectSFID, params.ClaGroupID) if err != nil { return gerrits.NewAddGerritBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) } @@ -111,11 +129,9 @@ func Configure(api *operations.EasyclaAPI, v1Service v1Gerrits.Service, projectS // add the gerrit addGerritInput := &v1Models.AddGerritInput{ - GerritName: params.AddGerritInput.GerritName, - GerritURL: params.AddGerritInput.GerritURL, - GroupIDCcla: params.AddGerritInput.GroupIDCcla, - GroupIDIcla: params.AddGerritInput.GroupIDIcla, - Version: "v2", + GerritName: params.AddGerritInput.GerritName, + GerritURL: params.AddGerritInput.GerritURL, + Version: "v2", } result, err := v1Service.AddGerrit(ctx, params.ClaGroupID, params.ProjectSFID, addGerritInput, projectModel) if err != nil { @@ -126,7 +142,7 @@ func Configure(api *operations.EasyclaAPI, v1Service v1Gerrits.Service, projectS } // record the event - eventService.LogEvent(&events.LogEventArgs{ + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.GerritRepositoryAdded, ProjectID: params.ClaGroupID, LfUsername: authUser.UserName, @@ -148,39 +164,52 @@ func Configure(api *operations.EasyclaAPI, v1Service v1Gerrits.Service, projectS reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + f := logrus.Fields{ + "functionName": "v2.gerrits.handlers.GerritsListGerritsHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": params.ProjectSFID, + "claGroupID": params.ClaGroupID, + "authUserName": authUser.UserName, + "authUserEmail": authUser.Email, + } // verify user have access to the project - if !utils.IsUserAuthorizedForProjectTree(authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { - return gerrits.NewListGerritsForbidden().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "403", - Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to ListGerrits with Project scope of %s", - authUser.UserName, params.ProjectSFID), - XRequestID: reqID, - }) + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to list gerrits with Project scope of %s", authUser.UserName, params.ProjectSFID) + log.WithFields(f).Warn(msg) + return gerrits.NewListGerritsForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } - ok, err := projectsClaGroupsRepo.IsAssociated(params.ProjectSFID, params.ClaGroupID) + log.WithFields(f).Debug("checking if project CLA Group mapping...") + ok, err := projectsClaGroupsRepo.IsAssociated(ctx, params.ProjectSFID, params.ClaGroupID) if err != nil { - return gerrits.NewListGerritsBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) + msg := fmt.Sprintf("unable to determine project CLA group association for project: %s and CLA Group: %s", params.ProjectSFID, params.ClaGroupID) + log.WithFields(f).WithError(err).Warn(msg) + return gerrits.NewListGerritsBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } + if !ok { - return gerrits.NewListGerritsBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "400", - Message: "provided cla-group and project are not associated with each other", - XRequestID: reqID, - }) + msg := fmt.Sprintf("provided CLA Group %s and project %s are not associated with each other", params.ProjectSFID, params.ClaGroupID) + log.WithFields(f).WithError(err).Warn(msg) + return gerrits.NewListGerritsBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequest(reqID, msg)) } - result, err := v1Service.GetClaGroupGerrits(ctx, params.ClaGroupID, ¶ms.ProjectSFID) + log.WithFields(f).Debug("querying for gerrits...") + result, err := v1Service.GetClaGroupGerrits(ctx, params.ClaGroupID) if err != nil { + msg := fmt.Sprintf("problem fetching gerrit repositories using CLA Group: %s with project SFID: %s", params.ClaGroupID, params.ProjectSFID) + log.WithFields(f).Warn(msg) return gerrits.NewListGerritsBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) } + log.WithFields(f).Debugf("discovered %d gerrits", len(result.List)) var response models.GerritList err = copier.Copy(&response, result) if err != nil { - return gerrits.NewListGerritsInternalServerError().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) + log.WithFields(f).WithError(err).Warn(decodeErrorMsg) + return gerrits.NewListGerritsInternalServerError().WithXRequestID(reqID).WithPayload(utils.ErrorResponseInternalServerErrorWithError(reqID, decodeErrorMsg, err)) } + return gerrits.NewListGerritsOK().WithXRequestID(reqID).WithPayload(&response) }) @@ -189,39 +218,237 @@ func Configure(api *operations.EasyclaAPI, v1Service v1Gerrits.Service, projectS reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + f := logrus.Fields{ + "functionName": "v2.gerrits.handlers.GerritsGetGerritReposHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "authUserName": authUser.UserName, + "authUserEmail": authUser.Email, + "gerritHost": params.GerritHost.String(), + } // No specific permissions required // Validate input if params.GerritHost == nil { - return gerrits.NewGetGerritReposBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "400", - Message: "missing gerritHost query parameter - expecting gerrit hostname", - XRequestID: reqID, - }) + msg := "missing gerrit host query parameter - expecting gerrit hostname" + log.WithFields(f).Warn(msg) + return gerrits.NewGetGerritReposBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequest(reqID, msg)) } if len(strings.TrimSpace(params.GerritHost.String())) == 0 { - return gerrits.NewGetGerritReposBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "400", - Message: "invalid gerritHost query parameter - expecting gerrit hostname", - XRequestID: reqID, - }) + msg := "invalid gerritHost query parameter - expecting gerrit hostname" + log.WithFields(f).Warn(msg) + return gerrits.NewGetGerritReposBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequest(reqID, msg)) } + log.WithFields(f).Debugf("querying for gerrits using hostname: %s...", params.GerritHost.String()) result, err := v1Service.GetGerritRepos(ctx, params.GerritHost.String()) if err != nil { - return gerrits.NewGetGerritReposBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) + msg := fmt.Sprintf("problem fetching gerrit repositories using gerrit host: %s", params.GerritHost.String()) + log.WithFields(f).Warn(msg) + return gerrits.NewGetGerritReposBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } var response models.GerritRepoList err = copier.Copy(&response, result) if err != nil { - return gerrits.NewAddGerritInternalServerError().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) + log.WithFields(f).WithError(err).Warn(decodeErrorMsg) + return gerrits.NewAddGerritInternalServerError().WithXRequestID(reqID).WithPayload(utils.ErrorResponseInternalServerErrorWithError(reqID, decodeErrorMsg, err)) } return gerrits.NewGetGerritReposOK().WithXRequestID(reqID).WithPayload(&response) }) + + // api.GerritsGetGerritICLAUserHandler = gerrits.GetGerritICLAUserHandlerFunc(func(params gerrits.GetGerritICLAUserParams, authUser *auth.User) middleware.Responder { + // reqID := utils.GetRequestID(params.XREQUESTID) + // ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + // utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + // f := logrus.Fields{ + // "functionName": "v2.gerrits.handlers.GerritsGetGerritICLAUserHandler", + // utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + // "authUserName": authUser.UserName, + // "authUserEmail": authUser.Email, + // "claGroupID": params.ClaGroupID, + // "projectSFID": params.ProjectSFID, + // } + + // // verify user have access to the project + // if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + // msg := fmt.Sprintf("user %s does not have access to get gerrit users with Project scope of %s", authUser.UserName, params.ProjectSFID) + // log.WithFields(f).Warn(msg) + // return gerrits.NewGetGerritICLAUserForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) + // } + + // log.WithFields(f).Debugf("getting user list to gerrit...") + // responseModel, err := v1Service.GetUsersOfGroup(ctx, authUser, params.ClaGroupID, utils.ClaTypeICLA) + // if err != nil { + // msg := fmt.Sprintf("problem getting user list of CLA Group %s", params.ClaGroupID) + // log.WithFields(f).WithError(err).Warn(msg) + // return gerrits.NewGetGerritICLAUserInternalServerError().WithXRequestID(reqID).WithPayload(utils.ErrorResponseInternalServerErrorWithError(reqID, msg, err)) + // } + + // return gerrits.NewGetGerritICLAUserOK().WithXRequestID(reqID).WithPayload(responseModel) + // }) + + // api.GerritsGetGerritECLAUserHandler = gerrits.GetGerritECLAUserHandlerFunc(func(params gerrits.GetGerritECLAUserParams, authUser *auth.User) middleware.Responder { + // reqID := utils.GetRequestID(params.XREQUESTID) + // ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + // utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + // f := logrus.Fields{ + // "functionName": "v2.gerrits.handlers.GerritsGetGerritECLAUserHandler", + // utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + // "authUserName": authUser.UserName, + // "authUserEmail": authUser.Email, + // "claGroupID": params.ClaGroupID, + // "projectSFID": params.ProjectSFID, + // } + + // // verify user have access to the project + // if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + // msg := fmt.Sprintf("user %s does not have access to get gerrit users with Project scope of %s", authUser.UserName, params.ProjectSFID) + // log.WithFields(f).Warn(msg) + // return gerrits.NewGetGerritECLAUserForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) + // } + + // log.WithFields(f).Debugf("getting user list to gerrit...") + // responseModel, err := v1Service.GetUsersOfGroup(ctx, authUser, params.ClaGroupID, utils.ClaTypeECLA) + // if err != nil { + // msg := fmt.Sprintf("problem getting user list of CLA Group %s", params.ClaGroupID) + // log.WithFields(f).WithError(err).Warn(msg) + // return gerrits.NewGetGerritECLAUserInternalServerError().WithXRequestID(reqID).WithPayload(utils.ErrorResponseInternalServerErrorWithError(reqID, msg, err)) + // } + + // return gerrits.NewGetGerritECLAUserOK().WithXRequestID(reqID).WithPayload(responseModel) + // }) + + // api.GerritsAddGerritICLAUserHandler = gerrits.AddGerritICLAUserHandlerFunc(func(params gerrits.AddGerritICLAUserParams, authUser *auth.User) middleware.Responder { + // reqID := utils.GetRequestID(params.XREQUESTID) + // ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + // utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + // f := logrus.Fields{ + // "functionName": "v2.gerrits.handlers.GerritsAddGerritICLAUserHandler", + // utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + // "authUserName": authUser.UserName, + // "authUserEmail": authUser.Email, + // "claGroupID": params.ClaGroupID, + // "projectSFID": params.ProjectSFID, + // "gerritUsers": strings.Join(params.AddGerritUserInput, ","), + // } + + // // verify user have access to the project + // if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + // msg := fmt.Sprintf("user %s does not have access to add gerrit users with Project scope of %s", authUser.UserName, params.ProjectSFID) + // log.WithFields(f).Warn(msg) + // return gerrits.NewAddGerritICLAUserForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) + // } + + // log.WithFields(f).Debugf("adding user list to gerrit...") + // err := v1Service.AddUsersToGroup(ctx, authUser, params.ClaGroupID, params.AddGerritUserInput, utils.ClaTypeICLA) + // if err != nil { + // msg := fmt.Sprintf("problem adding user list %s to CLA Group %s", strings.Join(params.AddGerritUserInput, ","), params.ClaGroupID) + // log.WithFields(f).WithError(err).Warn(msg) + // return gerrits.NewAddGerritICLAUserInternalServerError().WithXRequestID(reqID).WithPayload(utils.ErrorResponseInternalServerErrorWithError(reqID, msg, err)) + // } + + // return gerrits.NewAddGerritICLAUserOK().WithXRequestID(reqID) + // }) + + // api.GerritsRemoveGerritICLAUserHandler = gerrits.RemoveGerritICLAUserHandlerFunc(func(params gerrits.RemoveGerritICLAUserParams, authUser *auth.User) middleware.Responder { + // reqID := utils.GetRequestID(params.XREQUESTID) + // ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + // utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + // f := logrus.Fields{ + // "functionName": "v2.gerrits.handlers.GerritsRemoveGerritICLAUserHandler", + // utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + // "authUserName": authUser.UserName, + // "authUserEmail": authUser.Email, + // "claGroupID": params.ClaGroupID, + // "projectSFID": params.ProjectSFID, + // "gerritUsers": strings.Join(params.RemoveGerritUserInput, ","), + // } + + // // verify user have access to the project + // if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + // msg := fmt.Sprintf("user %s does not have access to remove gerrit users with Project scope of %s", authUser.UserName, params.ProjectSFID) + // log.WithFields(f).Warn(msg) + // return gerrits.NewRemoveGerritICLAUserForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) + // } + + // log.WithFields(f).Debugf("removing user list from gerrit...") + // err := v1Service.RemoveUsersFromGroup(ctx, authUser, params.ClaGroupID, params.RemoveGerritUserInput, utils.ClaTypeICLA) + // if err != nil { + // msg := fmt.Sprintf("problem removing user list %s to CLA Group %s", strings.Join(params.RemoveGerritUserInput, ","), params.ClaGroupID) + // log.WithFields(f).WithError(err).Warn(msg) + // return gerrits.NewRemoveGerritICLAUserInternalServerError().WithXRequestID(reqID).WithPayload(utils.ErrorResponseInternalServerErrorWithError(reqID, msg, err)) + // } + + // return gerrits.NewRemoveGerritICLAUserOK().WithXRequestID(reqID) + // }) + + // api.GerritsAddGerritECLAUserHandler = gerrits.AddGerritECLAUserHandlerFunc(func(params gerrits.AddGerritECLAUserParams, authUser *auth.User) middleware.Responder { + // reqID := utils.GetRequestID(params.XREQUESTID) + // ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + // utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + // f := logrus.Fields{ + // "functionName": "v2.gerrits.handlers.GerritsAddGerritECLAUserHandler", + // utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + // "authUserName": authUser.UserName, + // "authUserEmail": authUser.Email, + // "claGroupID": params.ClaGroupID, + // "projectSFID": params.ProjectSFID, + // "gerritUsers": strings.Join(params.AddGerritUserInput, ","), + // } + + // // verify user have access to the project + // if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + // msg := fmt.Sprintf("user %s does not have access to add gerrit users with Project scope of %s", authUser.UserName, params.ProjectSFID) + // log.WithFields(f).Warn(msg) + // return gerrits.NewAddGerritECLAUserForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) + // } + + // log.WithFields(f).Debugf("adding user list to gerrit...") + // err := v1Service.AddUsersToGroup(ctx, authUser, params.ClaGroupID, params.AddGerritUserInput, utils.ClaTypeECLA) + // if err != nil { + // msg := fmt.Sprintf("problem adding user list %s to CLA Group %s", strings.Join(params.AddGerritUserInput, ","), params.ClaGroupID) + // log.WithFields(f).WithError(err).Warn(msg) + // return gerrits.NewAddGerritECLAUserInternalServerError().WithXRequestID(reqID).WithPayload(utils.ErrorResponseInternalServerErrorWithError(reqID, msg, err)) + // } + + // return gerrits.NewAddGerritECLAUserOK().WithXRequestID(reqID) + // }) + + // api.GerritsRemoveGerritECLAUserHandler = gerrits.RemoveGerritECLAUserHandlerFunc(func(params gerrits.RemoveGerritECLAUserParams, authUser *auth.User) middleware.Responder { + // reqID := utils.GetRequestID(params.XREQUESTID) + // ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + // utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + // f := logrus.Fields{ + // "functionName": "v2.gerrits.handlers.GerritsRemoveGerritECLAUserHandler", + // utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + // "authUserName": authUser.UserName, + // "authUserEmail": authUser.Email, + // "claGroupID": params.ClaGroupID, + // "projectSFID": params.ProjectSFID, + // "gerritUsers": strings.Join(params.RemoveGerritUserInput, ","), + // } + + // // verify user have access to the project + // if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + // msg := fmt.Sprintf("user %s does not have access to remove gerrit users with Project scope of %s", authUser.UserName, params.ProjectSFID) + // log.WithFields(f).Warn(msg) + // return gerrits.NewRemoveGerritECLAUserForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) + // } + + // log.WithFields(f).Debugf("removing user list from gerrit...") + // err := v1Service.RemoveUsersFromGroup(ctx, authUser, params.ClaGroupID, params.RemoveGerritUserInput, utils.ClaTypeECLA) + // if err != nil { + // msg := fmt.Sprintf("problem removing user list %s to CLA Group %s", strings.Join(params.RemoveGerritUserInput, ","), params.ClaGroupID) + // log.WithFields(f).WithError(err).Warn(msg) + // return gerrits.NewRemoveGerritECLAUserInternalServerError().WithXRequestID(reqID).WithPayload(utils.ErrorResponseInternalServerErrorWithError(reqID, msg, err)) + // } + + // return gerrits.NewRemoveGerritECLAUserOK().WithXRequestID(reqID) + // }) + } type codedResponse interface { diff --git a/cla-backend-go/v2/github_activity/handlers.go b/cla-backend-go/v2/github_activity/handlers.go index 92027822b..e9f86fdb1 100644 --- a/cla-backend-go/v2/github_activity/handlers.go +++ b/cla-backend-go/v2/github_activity/handlers.go @@ -11,7 +11,7 @@ import ( log "github.com/communitybridge/easycla/cla-backend-go/logging" - "github.com/google/go-github/v33/github" // with go modules enabled (GO111MODULE=on or outside GOPATH)0:w + "github.com/google/go-github/v37/github" // with go modules enabled (GO111MODULE=on or outside GOPATH)0:w "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations" diff --git a/cla-backend-go/v2/github_activity/service.go b/cla-backend-go/v2/github_activity/service.go index ccf9898ee..3233966e9 100644 --- a/cla-backend-go/v2/github_activity/service.go +++ b/cla-backend-go/v2/github_activity/service.go @@ -9,7 +9,15 @@ import ( "fmt" "strconv" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" + "github.com/aws/aws-sdk-go/aws" + "github.com/communitybridge/easycla/cla-backend-go/emails" + v1GithubOrg "github.com/communitybridge/easycla/cla-backend-go/github_organizations" + + "github.com/sirupsen/logrus" + + "github.com/communitybridge/easycla/cla-backend-go/utils" + + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" "github.com/communitybridge/easycla/cla-backend-go/v2/dynamo_events" @@ -18,7 +26,7 @@ import ( "github.com/communitybridge/easycla/cla-backend-go/repositories" log "github.com/communitybridge/easycla/cla-backend-go/logging" - "github.com/google/go-github/v33/github" + "github.com/google/go-github/v37/github" ) // Service is responsible for handling the github activity events @@ -28,41 +36,83 @@ type Service interface { } type eventHandlerService struct { - githubRepo repositories.Repository + gitV1Repository repositories.RepositoryInterface + githubOrgRepo v1GithubOrg.RepositoryInterface eventService events.Service autoEnableService dynamo_events.AutoEnableService + emailService emails.Service + sendEmail bool } // NewService creates a new instance of the Event Handler Service -func NewService(githubRepo repositories.Repository, +func NewService(gitV1Repository repositories.RepositoryInterface, + githubOrgRepo v1GithubOrg.RepositoryInterface, + eventService events.Service, + autoEnableService dynamo_events.AutoEnableService, + emailService emails.Service) Service { + + return newService(gitV1Repository, githubOrgRepo, eventService, autoEnableService, emailService, true) +} + +func newService(gitV1Repository repositories.RepositoryInterface, + githubOrgRepo v1GithubOrg.RepositoryInterface, eventService events.Service, - autoEnableService dynamo_events.AutoEnableService) Service { + autoEnableService dynamo_events.AutoEnableService, + emailService emails.Service, + sendEmail bool) Service { return &eventHandlerService{ - githubRepo: githubRepo, + gitV1Repository: gitV1Repository, + githubOrgRepo: githubOrgRepo, eventService: eventService, autoEnableService: autoEnableService, + emailService: emailService, + sendEmail: sendEmail, } } func (s *eventHandlerService) ProcessRepositoryEvent(event *github.RepositoryEvent) error { - log.Debugf("ProcessRepositoryEvent called for action : %s", *event.Action) + ctx := utils.NewContext() + f := logrus.Fields{ + "functionName": "v2.github_activity.service.ProcessRepositoryEvent", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } if event.Action == nil { return fmt.Errorf("no action found in event payload") } + + if event.Repo == nil { + return fmt.Errorf("missing repository object in event payload") + } + + log.Debugf("ProcessRepositoryEvent called for action : %s for repository : %s", *event.Action, *event.Repo.Name) switch *event.Action { case "created": - return s.handleRepositoryAddedAction(event.Sender, event.Repo) + return s.handleRepositoryAddedAction(ctx, event.Sender, event.Repo) + case "renamed": + return s.handleRepositoryRenamedAction(ctx, event.Sender, event.Repo) + case "transferred": + if event.Org == nil { + return fmt.Errorf("missing org object in event payload") + } + return s.handleRepositoryTransferredAction(ctx, event.Sender, event.Repo, event.Org) case "deleted": - return s.handleRepositoryRemovedAction(event.Sender, event.Repo) + return s.handleRepositoryRemovedAction(ctx, event.Sender, event.Repo) + case "archived": + return s.handleRepositoryArchivedAction(ctx, event.Sender, event.Repo) default: - log.Warnf("ProcessRepositoryEvent no handler for action : %s", *event.Action) + log.WithFields(f).Warnf("no handler for action : %s", *event.Action) } return nil } -func (s *eventHandlerService) handleRepositoryAddedAction(sender *github.User, repo *github.Repository) error { +func (s *eventHandlerService) handleRepositoryAddedAction(ctx context.Context, sender *github.User, repo *github.Repository) error { + f := logrus.Fields{ + "functionName": "v2.github_activity.service.handleRepositoryAddedAction", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + if repo.ID == nil || *repo.ID == 0 { return fmt.Errorf("missing repo id") } @@ -74,30 +124,32 @@ func (s *eventHandlerService) handleRepositoryAddedAction(sender *github.User, r if repo.FullName == nil || *repo.FullName == "" { return fmt.Errorf("repo full name missing") } + repoModel, err := s.autoEnableService.CreateAutoEnabledRepository(repo) if err != nil { if errors.Is(err, dynamo_events.ErrAutoEnabledOff) { - log.Warnf("autoEnable is off for this repo : %s can't continue", *repo.FullName) + log.WithFields(f).Warnf("autoEnable is off for this repo : %s can't continue", *repo.FullName) return nil } return err } - if err := s.autoEnableService.NotifyCLAManagerForRepos(repoModel.RepositoryProjectID, []*models.GithubRepository{repoModel}); err != nil { - log.Warnf("notifyCLAManager for autoEnabled repo : %s for claGroup : %s failed : %v", repoModel.RepositoryName, repoModel.RepositoryProjectID, err) + if err := s.autoEnableService.NotifyCLAManagerForRepos(repoModel.RepositoryClaGroupID, []*models.GithubRepository{repoModel}); err != nil { + log.WithFields(f).Warnf("notifyCLAManager for autoEnabled repo : %s for claGroup : %s failed : %v", repoModel.RepositoryName, repoModel.RepositoryClaGroupID, err) } if sender == nil || sender.Login == nil || *sender.Login == "" { - log.Warnf("not able to send event empty sender") + log.WithFields(f).Warnf("not able to send event empty sender") return nil } // sending the log event for the added repository log.Debugf("handleRepositoryAddedAction sending RepositoryAdded Event for repo %s", *repo.FullName) - s.eventService.LogEvent(&events.LogEventArgs{ - EventType: events.RepositoryAdded, - ProjectID: repoModel.RepositoryProjectID, - UserID: *sender.Login, + s.eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.RepositoryAdded, + ProjectSFID: repoModel.RepositoryProjectSfid, + CLAGroupID: repoModel.RepositoryClaGroupID, + UserID: *sender.Login, EventData: &events.RepositoryAddedEventData{ RepositoryName: *repo.FullName, }, @@ -106,39 +158,351 @@ func (s *eventHandlerService) handleRepositoryAddedAction(sender *github.User, r return nil } -func (s *eventHandlerService) handleRepositoryRemovedAction(sender *github.User, repo *github.Repository) error { +func (s *eventHandlerService) handleRepositoryRemovedAction(ctx context.Context, sender *github.User, repo *github.Repository) error { + f := logrus.Fields{ + "functionName": "v2.github_activity.service.handleRepositoryRemovedAction", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + if repo.ID == nil || *repo.ID == 0 { return fmt.Errorf("missing repo id") } repositoryExternalID := strconv.FormatInt(*repo.ID, 10) - repoModel, err := s.githubRepo.GetRepositoryByGithubID(context.Background(), repositoryExternalID, true) + repoModel, err := s.gitV1Repository.GitHubGetRepositoryByExternalID(context.Background(), repositoryExternalID) if err != nil { - if errors.Is(err, repositories.ErrGithubRepositoryNotFound) { - log.Warnf("event for non existing local repo : %s, nothing to do", *repo.FullName) + if _, ok := err.(*utils.GitHubRepositoryNotFound); ok { + log.WithFields(f).Warnf("event for non existing local repo : %s, nothing to do", *repo.FullName) return nil } return fmt.Errorf("fetching the repo : %s by external id : %s failed : %v", *repo.FullName, repositoryExternalID, err) } + if !repoModel.Enabled { + log.WithFields(f).Infof("repo : %s already disabled, set repository as remote deleted", repoModel.RepositoryID) + err = s.gitV1Repository.GitHubSetRemoteDeletedRepository(ctx, repoModel.RepositoryID, true, false) + if err != nil { + return fmt.Errorf("setting repo : %s remote deleted failed : %v", *repo.FullName, err) + } + return nil + } + err = s.gitV1Repository.GitHubSetRemoteDeletedRepository(ctx, repoModel.RepositoryID, true, true) + if err != nil { + return fmt.Errorf("setting repo : %s remote deleted failed : %v", *repo.FullName, err) + } + log.WithFields(f).Infof("disabling repo : %s", repoModel.RepositoryID) + if err := s.gitV1Repository.GitHubDisableRepository(context.Background(), repoModel.RepositoryID); err != nil { + log.WithFields(f).Warnf("disabling repo : %s failed : %v", *repo.FullName, err) + return err + } + // sending event for the action + s.eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.RepositoryDisabled, + ProjectSFID: repoModel.RepositoryProjectSfid, + CLAGroupID: repoModel.RepositoryClaGroupID, + UserID: *sender.Login, + EventData: &events.RepositoryDisabledEventData{ + RepositoryName: *repo.FullName, + }, + }) + + if s.sendEmail { + subject := fmt.Sprintf("EasyCLA: Github Repository Was Removed") + body, err := emails.RenderGithubRepositoryDisabledTemplate(s.emailService, repoModel.RepositoryClaGroupID, emails.GithubRepositoryDisabledTemplateParams{ + GithubRepositoryActionTemplateParams: emails.GithubRepositoryActionTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: "CLA Manager", + }, + RepositoryName: repoModel.RepositoryName, + }, + GithubAction: "deleted", + }) - if err := s.githubRepo.DisableRepository(context.Background(), repoModel.RepositoryID); err != nil { - log.Warnf("disabling repo : %s failed : %v", *repo.FullName, err) + if err != nil { + log.WithFields(f).Warnf("rendering email template failed : %v", err) + return nil + } + + if err := s.emailService.NotifyClaManagersForClaGroupID(context.Background(), repoModel.RepositoryClaGroupID, subject, body); err != nil { + log.WithFields(f).Warnf("notifying cla managers via email failed : %v", err) + } + + } + + return nil +} + +// handles the event when a repository is renamed so we rename the repo in our records as well +func (s *eventHandlerService) handleRepositoryRenamedAction(ctx context.Context, sender *github.User, repo *github.Repository) error { + f := logrus.Fields{ + "functionName": "v2.github_activity.service.handleRepositoryRenamedAction", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + if repo.ID == nil || *repo.ID == 0 { + return fmt.Errorf("missing repo id") + } + repositoryExternalID := strconv.FormatInt(*repo.ID, 10) + repoModel, err := s.gitV1Repository.GitHubGetRepositoryByGithubID(context.Background(), repositoryExternalID, true) + if err != nil { + if _, ok := err.(*utils.GitHubRepositoryNotFound); ok { + log.WithFields(f).Warnf("event for non existing local repo : %s, nothing to do", *repo.FullName) + return nil + } + return fmt.Errorf("fetching the repo : %s by external id : %s failed : %v", *repo.FullName, repositoryExternalID, err) + } + + log.WithFields(f).Infof("renaming Github Repository from : %s to : %s", repoModel.RepositoryName, *repo.Name) + + if _, err := s.gitV1Repository.GitHubUpdateRepository(ctx, repoModel.RepositoryID, "", "", &models.GithubRepositoryInput{ + RepositoryName: repo.Name, + Note: "repository was renamed externally", + }); err != nil { + log.WithFields(f).Warnf("renaming repo : %s failed : %v", *repo.FullName, err) return err } + if sender == nil || sender.Login == nil { + return fmt.Errorf("missing sender can not log the event") + } + + // sending event for the action + s.eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.RepositoryRenamed, + ProjectSFID: repoModel.RepositoryProjectSfid, + CLAGroupID: repoModel.RepositoryClaGroupID, + UserID: *sender.Login, + EventData: &events.RepositoryRenamedEventData{ + NewRepositoryName: *repo.Name, + OldRepositoryName: repoModel.RepositoryName, + }, + }) + + if s.sendEmail { + subject := fmt.Sprintf("EasyCLA: Github Repository Was Renamed") + body, err := emails.RenderGithubRepositoryRenamedTemplate(s.emailService, repoModel.RepositoryClaGroupID, emails.GithubRepositoryRenamedTemplateParams{ + GithubRepositoryActionTemplateParams: emails.GithubRepositoryActionTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: "CLA Manager", + }, + RepositoryName: repoModel.RepositoryName, + }, + NewRepositoryName: *repo.Name, + OldRepositoryName: repoModel.RepositoryName, + }) + + if err != nil { + log.WithFields(f).Warnf("rendering email template failed : %v", err) + return nil + } + + if err := s.emailService.NotifyClaManagersForClaGroupID(context.Background(), repoModel.RepositoryClaGroupID, subject, body); err != nil { + log.WithFields(f).Warnf("notifying cla managers via email failed : %v", err) + } + + } + + return nil +} + +func (s *eventHandlerService) handleRepositoryTransferredAction(ctx context.Context, sender *github.User, repo *github.Repository, org *github.Organization) error { + if repo.Name == nil { + return fmt.Errorf("missing repo name can't proceed with transfer") + } + repoName := *repo.Name + + if org.Login == nil { + return fmt.Errorf("missing organization login information can't proceed with transferring the rpo : %s", *org.Name) + } + + f := logrus.Fields{ + "functionName": "v2.github_activity.service.handleRepositoryTransferredAction", + "repositoryName": repoName, + "newGithubOrganization": *org.Login, + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + if repo.ID == nil || *repo.ID == 0 { + return fmt.Errorf("missing repo id") + } + + repositoryExternalID := strconv.FormatInt(*repo.ID, 10) + repoModel, err := s.gitV1Repository.GitHubGetRepositoryByGithubID(context.Background(), repositoryExternalID, true) + if err != nil { + if _, ok := err.(*utils.GitHubRepositoryNotFound); ok { + log.WithFields(f).Warnf("event for non existing local repo : %s, nothing to do", repoName) + return nil + } + return fmt.Errorf("fetching the repo : %s by external id : %s failed : %v", repoName, repositoryExternalID, err) + } + + newOrganizationName := *org.Login + oldOrganizationName := repoModel.RepositoryOrganizationName + + log.WithFields(f).Infof("running transfer for repository : %s from Github Org : %s to Github Org : %s", repoName, oldOrganizationName, newOrganizationName) + + // first check if it's a different organization name (could be a duplicate event) + if oldOrganizationName == newOrganizationName { + msg := fmt.Sprintf("nothing to change for github repo : %s, probably duplicate event was sent", repoModel.RepositoryName) + log.WithFields(f).Warnf(msg) + return fmt.Errorf(msg) + } + + // fetch the old and the new github orgs from the db + oldGithubOrg, err := s.githubOrgRepo.GetGitHubOrganization(ctx, oldOrganizationName) + if err != nil { + return fmt.Errorf("fetching the old organization name : %s failed : %v", oldOrganizationName, err) + } + + newGithubOrg, err := s.githubOrgRepo.GetGitHubOrganization(ctx, newOrganizationName) + if err != nil { + disabledErr := s.disableFailedTransferRepo(ctx, sender, f, repoModel, oldGithubOrg, newGithubOrg) + if disabledErr != nil { + return disabledErr + } + + return fmt.Errorf("fetching the new organization name : %s failed : %v", newOrganizationName, err) + } + + // we need to check if the new org name has autoenabled and have a cla group set otherwise we can't proceed + if !newGithubOrg.AutoEnabled || newGithubOrg.AutoEnabledClaGroupID == "" { + disabledErr := s.disableFailedTransferRepo(ctx, sender, f, repoModel, oldGithubOrg, newGithubOrg) + if disabledErr != nil { + return disabledErr + } + + return fmt.Errorf("aborting the repository : %s transfer, new githubOrg : %s doesn't have claGroupID set", repoModel.RepositoryName, newGithubOrg.OrganizationName) + } + + _, err = s.gitV1Repository.GitHubUpdateRepository(ctx, repoModel.RepositoryID, "", "", &models.GithubRepositoryInput{ + Note: fmt.Sprintf("repository was transferred from org : %s to : %s", oldGithubOrg.OrganizationName, newGithubOrg.OrganizationName), + RepositoryOrganizationName: aws.String(newGithubOrg.OrganizationName), + RepositoryURL: repo.HTMLURL, + }) + + if err != nil { + return fmt.Errorf("repository : %s transfer failed for new github org : %s : %v", repoModel.RepositoryID, newGithubOrg.OrganizationName, err) + } + // sending event for the action - s.eventService.LogEvent(&events.LogEventArgs{ - EventType: events.RepositoryDisabled, - ProjectID: repoModel.RepositoryProjectID, - UserID: *sender.Login, + s.eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.RepositoryTransferred, + ProjectSFID: repoModel.RepositoryProjectSfid, + CLAGroupID: repoModel.RepositoryClaGroupID, + UserID: *sender.Login, + EventData: &events.RepositoryTransferredEventData{ + RepositoryName: repoModel.RepositoryName, + OldGithubOrgName: oldGithubOrg.OrganizationName, + NewGithubOrgName: newGithubOrg.OrganizationName, + }, + }) + + if s.sendEmail { + if err := s.notifyForGithubRepositoryTransferred(ctx, repoModel, oldGithubOrg, newGithubOrg, true); err != nil { + log.WithFields(f).Warnf("notifying cla managers via email failed : %v", err) + } + } + + return nil +} + +func (s *eventHandlerService) disableFailedTransferRepo(ctx context.Context, sender *github.User, f logrus.Fields, repoModel *models.GithubRepository, oldGithubOrg *models.GithubOrganization, newGithubOrg *models.GithubOrganization) error { + log.WithFields(f).Warnf("can't proceed with repo transfer operation because the new org doesn't have autoenabled=true, disabling the repo : %s", repoModel.RepositoryName) + if err := s.gitV1Repository.GitHubDisableRepository(ctx, repoModel.RepositoryID); err != nil { + return fmt.Errorf("disabling the repo : %s failed : %v", repoModel.RepositoryID, err) + } + + // send event for the disabled repository. + s.eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.RepositoryDisabled, + ProjectSFID: repoModel.RepositoryProjectSfid, + CLAGroupID: repoModel.RepositoryClaGroupID, + UserID: *sender.Login, EventData: &events.RepositoryDisabledEventData{ - RepositoryName: *repo.FullName, + RepositoryName: repoModel.RepositoryName, }, }) + if s.sendEmail { + if err := s.notifyForGithubRepositoryTransferred(ctx, repoModel, oldGithubOrg, newGithubOrg, false); err != nil { + log.WithFields(f).Warnf("notifying cla managers via email failed : %v", err) + } + } + return nil +} + +func (s *eventHandlerService) notifyForGithubRepositoryTransferred(ctx context.Context, repoModel *models.GithubRepository, oldGithubOrg *models.GithubOrganization, newGithubOrg *models.GithubOrganization, success bool) error { + subject := fmt.Sprintf("EasyCLA: Github Repository Was Transferred") + body, err := emails.RenderGithubRepositoryTransferredTemplate(s.emailService, repoModel.RepositoryClaGroupID, emails.GithubRepositoryTransferredTemplateParams{ + GithubRepositoryActionTemplateParams: emails.GithubRepositoryActionTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: "CLA Manager", + }, + RepositoryName: repoModel.RepositoryName, + }, + OldGithubOrgName: oldGithubOrg.OrganizationName, + NewGithubOrgName: newGithubOrg.OrganizationName, + }, success) + + if err != nil { + return fmt.Errorf("rendering email template failed : %v", err) + } + + err = s.emailService.NotifyClaManagersForClaGroupID(ctx, repoModel.RepositoryClaGroupID, subject, body) + return err +} + +func (s *eventHandlerService) handleRepositoryArchivedAction(ctx context.Context, sender *github.User, repo *github.Repository) error { + f := logrus.Fields{ + "functionName": "v2.github_activity.service.handleRepositoryArchivedAction", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + if repo.ID == nil || *repo.ID == 0 { + return fmt.Errorf("missing repo id") + } + repositoryExternalID := strconv.FormatInt(*repo.ID, 10) + repoModel, err := s.gitV1Repository.GitHubGetRepositoryByGithubID(context.Background(), repositoryExternalID, true) + if err != nil { + if _, ok := err.(*utils.GitHubRepositoryNotFound); ok { + log.WithFields(f).Warnf("event for non existing local repo : %s, nothing to do", *repo.FullName) + return nil + } + return fmt.Errorf("fetching the repo : %s by external id : %s failed : %v", *repo.FullName, repositoryExternalID, err) + } + + log.WithFields(f).Infof("archiving repository : %s", repoModel.RepositoryName) + + if s.sendEmail { + subject := fmt.Sprintf("EasyCLA: Github Repository Was Archived") + body, err := emails.RenderGithubRepositoryArchivedTemplate(s.emailService, repoModel.RepositoryClaGroupID, emails.GithubRepositoryArchivedTemplateParams{ + GithubRepositoryActionTemplateParams: emails.GithubRepositoryActionTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: "CLA Manager", + }, + RepositoryName: repoModel.RepositoryName, + }, + }) + + if err != nil { + log.WithFields(f).Warnf("rendering email template failed : %v", err) + return nil + } + + if err := s.emailService.NotifyClaManagersForClaGroupID(ctx, repoModel.RepositoryClaGroupID, subject, body); err != nil { + log.WithFields(f).Warnf("notifying cla managers via email failed : %v", err) + } + + } + return nil } func (s *eventHandlerService) ProcessInstallationRepositoriesEvent(event *github.InstallationRepositoriesEvent) error { + ctx := utils.NewContext() + f := logrus.Fields{ + "functionName": "v2.github_activity.service.ProcessInstallationRepositoriesEvent", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + log.Debugf("ProcessInstallationRepositoriesEvent called for action : %s", *event.Action) if event.Action == nil { return fmt.Errorf("no action found in event payload") @@ -146,28 +510,28 @@ func (s *eventHandlerService) ProcessInstallationRepositoriesEvent(event *github switch *event.Action { case "added": if len(event.RepositoriesAdded) == 0 { - log.Warnf("repositories list is empty nothing to add") + log.WithFields(f).Warnf("repositories list is empty nothing to add") return nil } for _, r := range event.RepositoriesAdded { - if err := s.handleRepositoryAddedAction(event.Sender, r); err != nil { + if err := s.handleRepositoryAddedAction(ctx, event.Sender, r); err != nil { // we just log it don't want to stop the whole process at this stage - log.Warnf("adding the repository : %s failed : %v", *r.FullName, err) + log.WithFields(f).Warnf("adding the repository : %s failed : %v", *r.FullName, err) } } case "removed": if len(event.RepositoriesRemoved) == 0 { - log.Warnf("repositories list is empty nothing to remove") + log.WithFields(f).Warnf("repositories list is empty nothing to remove") return nil } for _, r := range event.RepositoriesRemoved { - if err := s.handleRepositoryRemovedAction(event.Sender, r); err != nil { - log.Warnf("removing the repository : %s failed : %v", *r.FullName, err) + if err := s.handleRepositoryRemovedAction(ctx, event.Sender, r); err != nil { + log.WithFields(f).Warnf("removing the repository : %s failed : %v", *r.FullName, err) } } default: - log.Warnf("ProcessInstallationRepositoriesEvent no handler for action : %s", *event.Action) + log.WithFields(f).Warnf("ProcessInstallationRepositoriesEvent no handler for action : %s", *event.Action) } return nil diff --git a/cla-backend-go/v2/github_activity/service_test.go b/cla-backend-go/v2/github_activity/service_test.go new file mode 100644 index 000000000..341067b0d --- /dev/null +++ b/cla-backend-go/v2/github_activity/service_test.go @@ -0,0 +1,189 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package github_activity + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/communitybridge/easycla/cla-backend-go/events" + eventsMock "github.com/communitybridge/easycla/cla-backend-go/events/mock" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + githubOrgMock "github.com/communitybridge/easycla/cla-backend-go/github_organizations/mock" + "github.com/communitybridge/easycla/cla-backend-go/repositories/mock" + "github.com/golang/mock/gomock" + "github.com/google/go-github/v37/github" + "github.com/stretchr/testify/assert" +) + +func TestEventHandlerService_ProcessRepositoryEvent_HandleRepositoryRenamedAction(t *testing.T) { + repoID := "1f15f478-0659-43f3-bcf1-383052de7616" + repoName := "org1/repo-name" + newRepoName := "org1/repo-name-new" + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + githubOrganizationRepo := githubOrgMock.NewMockRepositoryInterface(ctrl) + githubRepo := mock.NewMockRepositoryInterface(ctrl) + githubRepo.EXPECT(). + GitHubGetRepositoryByGithubID(gomock.Any(), "1", true). + Return(&models.GithubRepository{ + Enabled: true, + RepositoryExternalID: 1, + RepositoryID: repoID, + RepositoryName: repoName, + RepositoryOrganizationName: "org1", + }, nil) + + githubRepo.EXPECT(). + GitHubUpdateRepository(gomock.Any(), repoID, "", "", &models.GithubRepositoryInput{ + RepositoryName: &newRepoName, + Note: "repository was renamed externally", + }).Return(nil, nil) + + eventsService := eventsMock.NewMockService(ctrl) + eventsService.EXPECT(). + LogEventWithContext(gomock.Any(), &events.LogEventArgs{ + EventType: events.RepositoryRenamed, + UserID: "githubLoginValue", + ProjectID: "", + EventData: &events.RepositoryRenamedEventData{ + NewRepositoryName: newRepoName, + OldRepositoryName: repoName, + }, + }).Return() + + activityService := newService(githubRepo, githubOrganizationRepo, eventsService, nil, nil, false) + err := activityService.ProcessRepositoryEvent(&github.RepositoryEvent{ + Action: aws.String("renamed"), + Repo: &github.Repository{ + ID: aws.Int64(1), + Name: &newRepoName, + }, + Org: nil, + Sender: &github.User{ + Login: aws.String("githubLoginValue"), + }, + Installation: nil, + }) + + assert.NoError(t, err) +} + +func TestEventHandlerService_ProcessRepositoryEvent_HandleRepositoryTransferredAction(t *testing.T) { + repoID := "1f15f478-0659-43f3-bcf1-383052de7616" + repoName := "org1/repo-name" + oldOrgName := "org1" + newOrgName := "org2" + newRepoUrl := "org2/repo-name" + + testCases := []struct { + name string + newGithubOrg *models.GithubOrganization + }{ + { + name: "success new org is enabled and and has cla group", + newGithubOrg: &models.GithubOrganization{ + OrganizationName: newOrgName, + AutoEnabled: true, + AutoEnabledClaGroupID: "c057ed9a-4235-4acf-80bd-c7b4c235eff9", + }, + }, + { + name: "failure new org is disabled and no cla group", + newGithubOrg: &models.GithubOrganization{ + OrganizationName: newOrgName, + AutoEnabled: false, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(tt *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + githubOrganizationRepo := githubOrgMock.NewMockRepositoryInterface(ctrl) + githubRepo := mock.NewMockRepositoryInterface(ctrl) + githubRepo.EXPECT(). + GitHubGetRepositoryByGithubID(gomock.Any(), "1", true). + Return(&models.GithubRepository{ + Enabled: true, + RepositoryExternalID: 1, + RepositoryID: repoID, + RepositoryName: repoName, + RepositoryOrganizationName: oldOrgName, + }, nil) + + // return the old one + githubOrganizationRepo.EXPECT(). + GetGitHubOrganization(gomock.Any(), oldOrgName). + Return(&models.GithubOrganization{ + OrganizationName: oldOrgName, + }, nil) + + // return the new one + githubOrganizationRepo.EXPECT(). + GetGitHubOrganization(gomock.Any(), newOrgName). + Return(tc.newGithubOrg, nil) + + eventsService := eventsMock.NewMockService(ctrl) + if tc.newGithubOrg.AutoEnabled { + githubRepo.EXPECT(). + GitHubUpdateRepository(gomock.Any(), repoID, gomock.Any(), gomock.Any(), &models.GithubRepositoryInput{ + RepositoryOrganizationName: &newOrgName, + RepositoryURL: &newRepoUrl, + Note: fmt.Sprintf("repository was transferred from org : %s to : %s", oldOrgName, newOrgName), + }).Return(nil, nil) + + eventsService.EXPECT(). + LogEventWithContext(gomock.Any(), &events.LogEventArgs{ + EventType: events.RepositoryTransferred, + UserID: "githubLoginValue", + ProjectID: "", + EventData: &events.RepositoryTransferredEventData{ + RepositoryName: repoName, + OldGithubOrgName: oldOrgName, + NewGithubOrgName: newOrgName, + }, + }).Return() + } else { + githubRepo.EXPECT(). + GitHubDisableRepository(gomock.Any(), repoID).Return(nil) + eventsService.EXPECT(). + LogEventWithContext(gomock.Any(), &events.LogEventArgs{ + EventType: events.RepositoryDisabled, + UserID: "githubLoginValue", + ProjectID: "", + EventData: &events.RepositoryDisabledEventData{ + RepositoryName: repoName, + }, + }).Return() + } + + activityService := newService(githubRepo, githubOrganizationRepo, eventsService, nil, nil, false) + err := activityService.ProcessRepositoryEvent(&github.RepositoryEvent{ + Action: aws.String("transferred"), + Repo: &github.Repository{ + ID: aws.Int64(1), + Name: &repoName, + HTMLURL: &newRepoUrl, + }, + Org: &github.Organization{ + Login: &newOrgName, + }, + Sender: &github.User{ + Login: aws.String("githubLoginValue"), + }, + Installation: nil, + }) + + if tc.newGithubOrg.AutoEnabled { + assert.NoError(tt, err) + } else { + assert.Error(tt, err) + } + }) + } +} diff --git a/cla-backend-go/v2/github_organizations/handlers.go b/cla-backend-go/v2/github_organizations/handlers.go index 116bfee01..1d53a10eb 100644 --- a/cla-backend-go/v2/github_organizations/handlers.go +++ b/cla-backend-go/v2/github_organizations/handlers.go @@ -13,7 +13,6 @@ import ( "github.com/LF-Engineering/lfx-kit/auth" "github.com/communitybridge/easycla/cla-backend-go/events" - "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations/github_organizations" "github.com/communitybridge/easycla/cla-backend-go/github" @@ -23,6 +22,7 @@ import ( // Configure setups handlers on api with service func Configure(api *operations.EasyclaAPI, service Service, eventService events.Service) { + api.GithubOrganizationsGetProjectGithubOrganizationsHandler = github_organizations.GetProjectGithubOrganizationsHandlerFunc( func(params github_organizations.GetProjectGithubOrganizationsParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) @@ -30,14 +30,14 @@ func Configure(api *operations.EasyclaAPI, service Service, eventService events. ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint f := logrus.Fields{ - "functionName": "GitHubOrganizationsGetProjectGithubOrganizationsHandler", + "functionName": "github_organizations.handlers.GitHubOrganizationsGetProjectGithubOrganizationsHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "authUser": authUser.UserName, "authEmail": authUser.Email, "projectSFID": params.ProjectSFID, } - if !utils.IsUserAuthorizedForProjectTree(authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { msg := fmt.Sprintf("user %s does not have access to Get Project GitHub Organizations with Project scope of %s", authUser.UserName, params.ProjectSFID) log.WithFields(f).Debug(msg) @@ -70,14 +70,14 @@ func Configure(api *operations.EasyclaAPI, service Service, eventService events. ctx := context.WithValue(params.HTTPRequest.Context(), utils.XREQUESTID, reqID) // nolint f := logrus.Fields{ - "functionName": "GitHubOrganizationsAddProjectGithubOrganizationHandler", + "functionName": "github_organization.handlers.GitHubOrganizationsAddProjectGithubOrganizationHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "authUser": authUser.UserName, "authEmail": authUser.Email, "projectSFID": params.ProjectSFID, } - if !utils.IsUserAuthorizedForProjectTree(authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { msg := fmt.Sprintf("user %s does not have access to Add Project GitHub Organizations with Project scope of %s", authUser.UserName, params.ProjectSFID) log.WithFields(f).Debug(msg) @@ -112,7 +112,7 @@ func Configure(api *operations.EasyclaAPI, service Service, eventService events. utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } - if !utils.ValidateAutoEnabledClaGroupID(params.Body.AutoEnabled, params.Body.AutoEnabledClaGroupID) { + if !utils.ValidateAutoEnabledClaGroupID(*params.Body.AutoEnabled, params.Body.AutoEnabledClaGroupID) { msg := "AutoEnabledClaGroupID can't be empty when AutoEnabled" log.WithFields(f).WithError(err).Warn(msg) return github_organizations.NewAddProjectGithubOrganizationBadRequest().WithPayload( @@ -128,10 +128,10 @@ func Configure(api *operations.EasyclaAPI, service Service, eventService events. } // Log the event - eventService.LogEvent(&events.LogEventArgs{ - LfUsername: authUser.UserName, - EventType: events.GitHubOrganizationAdded, - ExternalProjectID: params.ProjectSFID, + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + LfUsername: authUser.UserName, + EventType: events.GitHubOrganizationAdded, + ProjectSFID: params.ProjectSFID, EventData: &events.GitHubOrganizationAddedEventData{ GitHubOrganizationName: *params.Body.OrganizationName, }, @@ -145,32 +145,38 @@ func Configure(api *operations.EasyclaAPI, service Service, eventService events. reqID := utils.GetRequestID(params.XREQUESTID) utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + f := logrus.Fields{ + "functionName": "github_organization.handlers.GithubOrganizationsDeleteProjectGithubOrganizationHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": params.ProjectSFID, + "orgName": params.OrgName, + "authUser": authUser.UserName, + "authEmail": authUser.Email, + } - if !utils.IsUserAuthorizedForProjectTree(authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { - return github_organizations.NewDeleteProjectGithubOrganizationForbidden().WithPayload(&models.ErrorResponse{ - Code: "403", - Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to Delete Project GitHub Organizations with Project scope of %s", - authUser.UserName, params.ProjectSFID), - XRequestID: reqID, - }) + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to Delete Project GitHub Organizations with Project scope of %s", + authUser.UserName, params.ProjectSFID) + log.WithFields(f).Debug(msg) + return github_organizations.NewDeleteProjectGithubOrganizationForbidden().WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } err := service.DeleteGithubOrganization(ctx, params.ProjectSFID, params.OrgName) if err != nil { if strings.Contains(err.Error(), "getProjectNotFound") { - return github_organizations.NewDeleteProjectGithubOrganizationNotFound().WithPayload(&models.ErrorResponse{ - Code: "404", - Message: fmt.Sprintf("project not found with given ID. [%s]", params.ProjectSFID), - XRequestID: reqID, - }) + msg := fmt.Sprintf("project not found with given SFID: %s", params.ProjectSFID) + log.WithFields(f).Debug(msg) + return github_organizations.NewDeleteProjectGithubOrganizationNotFound().WithPayload(utils.ErrorResponseNotFoundWithError(reqID, msg, err)) } - return github_organizations.NewDeleteProjectGithubOrganizationBadRequest().WithPayload(errorResponse(reqID, err)) + msg := fmt.Sprintf("problem deleting GitHub Organization with project SFID: %s for organization: %s", params.ProjectSFID, params.OrgName) + log.WithFields(f).Debug(msg) + return github_organizations.NewDeleteProjectGithubOrganizationBadRequest().WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } - eventService.LogEvent(&events.LogEventArgs{ - LfUsername: authUser.UserName, - EventType: events.GitHubOrganizationDeleted, - ExternalProjectID: params.ProjectSFID, + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + LfUsername: authUser.UserName, + EventType: events.GitHubOrganizationDeleted, + ProjectSFID: params.ProjectSFID, EventData: &events.GitHubOrganizationDeletedEventData{ GitHubOrganizationName: params.OrgName, }, @@ -185,64 +191,54 @@ func Configure(api *operations.EasyclaAPI, service Service, eventService events. utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint - if !utils.IsUserAuthorizedForProjectTree(authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { - return github_organizations.NewUpdateProjectGithubOrganizationConfigForbidden().WithPayload(&models.ErrorResponse{ - Code: "403", - Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to Update Project GitHub Organizations with Project scope of %s", - authUser.UserName, params.ProjectSFID), - XRequestID: reqID, - }) + f := logrus.Fields{ + "functionName": "github_organization.handlers.GithubOrganizationsUpdateProjectGithubOrganizationConfigHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": params.ProjectSFID, + "orgName": params.OrgName, + "authUser": authUser.UserName, + "authEmail": authUser.Email, + } + + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to Update Project GitHub Organizations with Project scope of %s", + authUser.UserName, params.ProjectSFID) + log.WithFields(f).Debug(msg) + return github_organizations.NewUpdateProjectGithubOrganizationConfigForbidden().WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } if params.Body.AutoEnabled == nil { - return github_organizations.NewUpdateProjectGithubOrganizationConfigBadRequest().WithPayload(&models.ErrorResponse{ - Code: "400", - Message: "EasyCLA - 400 Bad Request - missing auto enable value in body", - XRequestID: reqID, - }) + msg := fmt.Sprintf("missing auto enable value in request body for project SFID: %s for organization: %s", params.ProjectSFID, params.OrgName) + log.WithFields(f).Debug(msg) + return github_organizations.NewUpdateProjectGithubOrganizationConfigBadRequest().WithPayload(utils.ErrorResponseBadRequest(reqID, msg)) } - if !utils.ValidateAutoEnabledClaGroupID(params.Body.AutoEnabled, params.Body.AutoEnabledClaGroupID) { - return github_organizations.NewAddProjectGithubOrganizationBadRequest().WithPayload(&models.ErrorResponse{ - Code: "400", - Message: "EasyCLA - 400 Bad Request - AutoEnabledClaGroupID can't be empty when AutoEnabled", - }) + if !utils.ValidateAutoEnabledClaGroupID(*params.Body.AutoEnabled, params.Body.AutoEnabledClaGroupID) { + msg := fmt.Sprintf("AutoEnabledClaGroupID can't be empty when AutoEnabled flag is set to true - issue in request body for project SFID: %s for organization: %s", params.ProjectSFID, params.OrgName) + log.WithFields(f).Debug(msg) + return github_organizations.NewUpdateProjectGithubOrganizationConfigBadRequest().WithPayload(utils.ErrorResponseBadRequest(reqID, msg)) } err := service.UpdateGithubOrganization(ctx, params.ProjectSFID, params.OrgName, *params.Body.AutoEnabled, params.Body.AutoEnabledClaGroupID, params.Body.BranchProtectionEnabled) if err != nil { - return github_organizations.NewUpdateProjectGithubOrganizationConfigBadRequest().WithPayload(errorResponse(reqID, err)) + msg := fmt.Sprintf("problem updating GitHub Organization for project SFID: %s for organization: %s", params.ProjectSFID, params.OrgName) + log.WithFields(f).Debug(msg) + return github_organizations.NewUpdateProjectGithubOrganizationConfigBadRequest().WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } - eventService.LogEvent(&events.LogEventArgs{ - LfUsername: authUser.UserName, - EventType: events.GitHubOrganizationUpdated, - ExternalProjectID: params.ProjectSFID, + // Log the event + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + LfUsername: authUser.UserName, + EventType: events.GitHubOrganizationUpdated, + ProjectSFID: params.ProjectSFID, EventData: &events.GitHubOrganizationUpdatedEventData{ - GitHubOrganizationName: params.OrgName, - AutoEnabled: *params.Body.AutoEnabled, + GitHubOrganizationName: params.OrgName, + AutoEnabled: utils.BoolValue(params.Body.AutoEnabled), + AutoEnabledClaGroupID: params.Body.AutoEnabledClaGroupID, + BranchProtectionEnabled: params.Body.BranchProtectionEnabled, }, }) return github_organizations.NewUpdateProjectGithubOrganizationConfigOK() }) } - -type codedResponse interface { - Code() string -} - -func errorResponse(reqID string, err error) *models.ErrorResponse { - code := "" - if e, ok := err.(codedResponse); ok { - code = e.Code() - } - - e := models.ErrorResponse{ - Code: code, - Message: err.Error(), - XRequestID: reqID, - } - - return &e -} diff --git a/cla-backend-go/v2/github_organizations/service.go b/cla-backend-go/v2/github_organizations/service.go index c469257e0..a82f9e904 100644 --- a/cla-backend-go/v2/github_organizations/service.go +++ b/cla-backend-go/v2/github_organizations/service.go @@ -6,22 +6,23 @@ package github_organizations import ( "context" "fmt" + "net/url" "sort" - "strconv" "strings" "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" + "github.com/go-openapi/strfmt" "github.com/sirupsen/logrus" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/utils" - v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/models" + v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" v1GithubOrg "github.com/communitybridge/easycla/cla-backend-go/github_organizations" - v1Repositories "github.com/communitybridge/easycla/cla-backend-go/repositories" + gitV1Repository "github.com/communitybridge/easycla/cla-backend-go/repositories" v2ProjectService "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" "github.com/jinzhu/copier" ) @@ -38,78 +39,107 @@ func v2GithubOrganizationModel(in *v1Models.GithubOrganization) (*models.GithubO // Service contains functions of GithubOrganizations service type Service interface { GetGithubOrganizations(ctx context.Context, projectSFID string) (*models.ProjectGithubOrganizations, error) - AddGithubOrganization(ctx context.Context, projectSFID string, input *models.CreateGithubOrganization) (*models.GithubOrganization, error) + AddGithubOrganization(ctx context.Context, projectSFID string, input *models.GithubCreateOrganization) (*models.GithubOrganization, error) DeleteGithubOrganization(ctx context.Context, projectSFID string, githubOrgName string) error UpdateGithubOrganization(ctx context.Context, projectSFID string, organizationName string, autoEnabled bool, autoEnabledClaGroupID string, branchProtectionEnabled bool) error } type service struct { - repo v1GithubOrg.Repository - ghRepository v1Repositories.Repository + repo v1GithubOrg.RepositoryInterface + gitV1Repository gitV1Repository.RepositoryInterface + ghService v1GithubOrg.ServiceInterface projectsCLAGroupService projects_cla_groups.Repository } // NewService creates a new githubOrganizations service -func NewService(repo v1GithubOrg.Repository, ghRepository v1Repositories.Repository, projectsCLAGroupService projects_cla_groups.Repository) Service { +func NewService(repo v1GithubOrg.RepositoryInterface, gitV1Repository gitV1Repository.RepositoryInterface, projectsCLAGroupService projects_cla_groups.Repository, ghService v1GithubOrg.ServiceInterface) Service { return service{ repo: repo, - ghRepository: ghRepository, + gitV1Repository: gitV1Repository, projectsCLAGroupService: projectsCLAGroupService, + ghService: ghService, } } -const ( - // Connected status - Connected = "connected" - // PartialConnection status - PartialConnection = "partial_connection" - // ConnectionFailure status - ConnectionFailure = "connection_failure" - // NoConnection status - NoConnection = "no_connection" -) - func (s service) GetGithubOrganizations(ctx context.Context, projectSFID string) (*models.ProjectGithubOrganizations, error) { f := logrus.Fields{ - "functionName": "GetGitHubOrganizations", + "functionName": "v2.github_organizations.service.GetGitHubOrganizations", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": projectSFID, } + var orgs *v1Models.GithubOrganizations + var orgErr error + pcg, pcgErr := s.projectsCLAGroupService.GetClaGroupIDForProject(ctx, projectSFID) + if pcgErr != nil { + if pcgErr == projects_cla_groups.ErrProjectNotAssociatedWithClaGroup { + log.WithFields(f).Warnf("unable to locate project CLA Group mapping for project SFID: %s, error: %+v", projectSFID, pcgErr) + } else { + log.WithFields(f).WithError(pcgErr).Warnf("unable to load project CLA group for project SFID: %s", projectSFID) + return nil, pcgErr + } + } + + if pcg != nil && pcg.FoundationSFID != "" { + log.WithFields(f).Debugf("Getting Github Organizations under foundation : %s", pcg.FoundationSFID) + orgs, orgErr = s.repo.GetGitHubOrganizationsByParent(ctx, pcg.FoundationSFID) + } else { + log.WithFields(f).Debugf("Getting Github Organizations under project : %s", projectSFID) + orgs, orgErr = s.repo.GetGitHubOrganizations(ctx, projectSFID) + } + + if orgErr != nil { + log.WithFields(f).Warnf("problem loading github organizations for project : %s, error: %+v", projectSFID, orgErr) + return nil, orgErr + } + + // Load the GitHub Organization and Repository details - result will be missing CLA Group info and ProjectSFID details + log.WithFields(f).Debugf("discovered %d GitHub organizations for projectSFID: %s", len(orgs.List), projectSFID) + orgs.List = s.ghService.RemoveDuplicates(orgs.List) psc := v2ProjectService.GetClient() log.WithFields(f).Debug("loading project details from the project service...") - projectServiceRecord, err := psc.GetProject(projectSFID) + project, err := psc.GetProject(projectSFID) + if err != nil { log.WithFields(f).WithError(err).Warn("problem loading project details from the project service") return nil, err } + if project == nil { + log.WithFields(f).Warnf("unable to load project by project SFID: %s", projectSFID) + return nil, nil + } + f["projectName"] = project.Name + f["projectType"] = project.Type + f["projectStatus"] = project.Status var parentProjectSFID string - if projectServiceRecord.Parent == "" || projectServiceRecord.Parent == utils.TheLinuxFoundation { + if !utils.IsProjectHaveParent(project) { parentProjectSFID = projectSFID } else { - parentProjectSFID = projectServiceRecord.Parent + parentProjectSFID = utils.GetProjectParentSFID(project) + // If we don't have a valid parent project SFID... + if parentProjectSFID == "" { + parentProjectSFID = projectSFID + } } + f["parentProjectSFID"] = parentProjectSFID log.WithFields(f).Debug("located parentProjectID...") - log.WithFields(f).Debug("loading github organization details by parentProjectSFID...") - orgs, err := s.repo.GetGithubOrganizationsByParent(ctx, parentProjectSFID) - // log.WithFields(f).Debug("loading github organization details by projectSFID...") - //orgs, err := s.repo.GetGithubOrganizations(ctx, projectSFID) - if err != nil { - log.WithFields(f).WithError(err).Warn("problem loading github organizations from the project service") - return nil, err - } - + // Our response model out := &models.ProjectGithubOrganizations{ List: make([]*models.ProjectGithubOrganization, 0), } + + // Next, we need to load a bunch of additional data for the response including the github status (if it's still connected/live, not renamed/moved), the CLA Group details, etc. + + // A temp data model for holding the intermediate results type githubRepoInfo struct { orgName string repoInfo *v1Models.GithubRepositoryInfo } - // connectedRepo contains list of repositories for which github app have permission + + // connectedRepo contains list of repositories for which github app have permission to see connectedRepo := make(map[string]*githubRepoInfo) orgmap := make(map[string]*models.ProjectGithubOrganization) for _, org := range orgs.List { @@ -124,7 +154,7 @@ func (s service) GetGithubOrganizations(ctx context.Context, projectSFID string) autoEnabledCLAGroupName := "" if org.AutoEnabledClaGroupID != "" { log.WithFields(f).Debugf("Loading CLA Group by ID: %s to obtain the name for GitHub auth enabled CLA Group response", org.AutoEnabledClaGroupID) - claGroupMode, claGroupLookupErr := s.projectsCLAGroupService.GetCLAGroup(org.AutoEnabledClaGroupID) + claGroupMode, claGroupLookupErr := s.projectsCLAGroupService.GetCLAGroup(ctx, org.AutoEnabledClaGroupID) if claGroupLookupErr != nil { log.WithFields(f).WithError(claGroupLookupErr).Warnf("Unable to lookup CLA Group by ID: %s", org.AutoEnabledClaGroupID) } @@ -133,6 +163,13 @@ func (s service) GetGithubOrganizations(ctx context.Context, projectSFID string) } } + installURL := url.URL{ + Scheme: "https", + Host: "github.com", + Path: fmt.Sprintf("/organizations/%s/settings/installations/%d", org.OrganizationName, org.OrganizationInstallationID), + } + installationURL := strfmt.URI(installURL.String()) + rorg := &models.ProjectGithubOrganization{ AutoEnabled: org.AutoEnabled, AutoEnableCLAGroupID: org.AutoEnabledClaGroupID, @@ -141,68 +178,91 @@ func (s service) GetGithubOrganizations(ctx context.Context, projectSFID string) ConnectionStatus: "", // updated below GithubOrganizationName: org.OrganizationName, Repositories: make([]*models.ProjectGithubRepository, 0), + InstallationURL: &installationURL, } orgmap[org.OrganizationName] = rorg out.List = append(out.List, rorg) if org.OrganizationInstallationID == 0 { - rorg.ConnectionStatus = NoConnection + rorg.ConnectionStatus = utils.NoConnection } else { if org.Repositories.Error != "" { - rorg.ConnectionStatus = ConnectionFailure + rorg.ConnectionStatus = utils.ConnectionFailure } else { - rorg.ConnectionStatus = Connected + rorg.ConnectionStatus = utils.Connected } } } - log.WithFields(f).Debug("listing github repositories...") - enabled := true - repos, err := s.ghRepository.ListProjectRepositories(ctx, parentProjectSFID, projectSFID, &enabled) - if err != nil { - log.WithFields(f).WithError(err).Warn("problem loading github repositories") - return nil, err + // We need to search the repository list based on two criteria + // Need to search by projectSFID and/or Organization ID???? + log.WithFields(f).Debugf("loading github repositories from %d organizations for projectSFID: %s...", len(orgs.List), projectSFID) + var repoList []*v1Models.GithubRepository + for _, org := range orgs.List { + orgRepos, orgReposErr := s.gitV1Repository.GitHubGetRepositoriesByOrganizationName(ctx, org.OrganizationName) + if orgReposErr != nil || len(orgRepos) == 0 { + if _, ok := orgReposErr.(*utils.GitHubRepositoryNotFound); ok { + log.WithFields(f).Debug(orgReposErr) + } else { + log.WithFields(f).WithError(orgReposErr).Warn("problem loading github repositories by org name") + } + } else { + repoList = append(repoList, orgRepos...) + } } - log.WithFields(f).Debugf("processing %d github repositories...", len(repos.List)) - for _, repo := range repos.List { + // Remove any duplicates + log.WithFields(f).Debugf("processing %d github repositories...", len(repoList)) + for _, repo := range repoList { + if repo == nil || repo.RepositoryOrganizationName == "" { + log.WithFields(f).Warnf("repositories record nil or is missing the organization name: %+v - skipping", repo) + continue + } + //log.WithFields(f).Debugf("processing repository: %s", repo.RepositoryURL) + rorg, ok := orgmap[repo.RepositoryOrganizationName] if !ok { log.WithFields(f).Warnf("repositories table contain stale data for organization %s", repo.RepositoryOrganizationName) continue } key := fmt.Sprintf("%s#%v", repo.RepositoryOrganizationName, repo.RepositoryExternalID) + + parentProjectSFID = repo.RepositoryProjectSfid + parentProjectModel, projectModelErr := v2ProjectService.GetClient().GetParentProjectModel(repo.RepositoryProjectSfid) + if projectModelErr == nil && parentProjectModel != nil { + parentProjectSFID = parentProjectModel.ID + } + if _, ok := connectedRepo[key]; ok { - repoGithubID, err := strconv.ParseInt(repo.RepositoryExternalID, 10, 64) - if err != nil { - log.WithFields(f).WithError(err).Warn("repository github id is not integer") - } + rorg.Repositories = append(rorg.Repositories, &models.ProjectGithubRepository{ - ConnectionStatus: Connected, - Enabled: true, + ConnectionStatus: utils.Connected, + Enabled: repo.Enabled, RepositoryID: repo.RepositoryID, RepositoryName: repo.RepositoryName, - RepositoryGithubID: repoGithubID, - ClaGroupID: repo.RepositoryProjectID, - ProjectID: repo.ProjectSFID, - ParentProjectID: repo.RepositorySfdcID, + RepositoryGithubID: repo.RepositoryExternalID, + ClaGroupID: repo.RepositoryClaGroupID, + ProjectID: repo.RepositoryProjectSfid, + ParentProjectID: parentProjectSFID, }) + // delete it from connectedRepo array since we have processed it // connectedArray after this loop will contain repo for which github app have permission but // they are enabled in cla delete(connectedRepo, key) } else { rorg.Repositories = append(rorg.Repositories, &models.ProjectGithubRepository{ - ConnectionStatus: ConnectionFailure, - Enabled: true, + ConnectionStatus: utils.ConnectionFailure, + Enabled: repo.Enabled, RepositoryID: repo.RepositoryID, RepositoryName: repo.RepositoryName, - ClaGroupID: repo.RepositoryProjectID, - ProjectID: repo.ProjectSFID, - ParentProjectID: repo.RepositorySfdcID, + ClaGroupID: repo.RepositoryClaGroupID, + ProjectID: repo.RepositoryProjectSfid, + ParentProjectID: parentProjectSFID, }) - if rorg.ConnectionStatus == Connected { - rorg.ConnectionStatus = PartialConnection + + if rorg.ConnectionStatus == utils.Connected { + rorg.ConnectionStatus = utils.PartialConnection } } } @@ -214,7 +274,7 @@ func (s service) GetGithubOrganizations(ctx context.Context, projectSFID string) continue } rorg.Repositories = append(rorg.Repositories, &models.ProjectGithubRepository{ - ConnectionStatus: Connected, + ConnectionStatus: utils.Connected, Enabled: false, RepositoryID: "", RepositoryName: notEnabledRepo.repoInfo.RepositoryName, @@ -235,9 +295,9 @@ func (s service) GetGithubOrganizations(ctx context.Context, projectSFID string) return out, nil } -func (s service) AddGithubOrganization(ctx context.Context, projectSFID string, input *models.CreateGithubOrganization) (*models.GithubOrganization, error) { +func (s service) AddGithubOrganization(ctx context.Context, projectSFID string, input *models.GithubCreateOrganization) (*models.GithubOrganization, error) { f := logrus.Fields{ - "functionName": "AddGitHubOrganization", + "functionName": "v2.github_organizations.service.AddGitHubOrganization", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": projectSFID, "autoEnabled": utils.BoolValue(input.AutoEnabled), @@ -245,7 +305,7 @@ func (s service) AddGithubOrganization(ctx context.Context, projectSFID string, "organizationName": utils.StringValue(input.OrganizationName), } - var in v1Models.CreateGithubOrganization + var in v1Models.GithubCreateOrganization err := copier.Copy(&in, input) if err != nil { log.WithFields(f).WithError(err).Warn("problem converting the github organization details") @@ -261,16 +321,17 @@ func (s service) AddGithubOrganization(ctx context.Context, projectSFID string, } var parentProjectSFID string - if project.Parent == "" || project.Parent == utils.TheLinuxFoundation { + if !utils.IsProjectHaveParent(project) || utils.IsProjectHasRootParent(project) || utils.GetProjectParentSFID(project) == "" { parentProjectSFID = projectSFID } else { - parentProjectSFID = project.Parent + parentProjectSFID = utils.GetProjectParentSFID(project) } + f["parentProjectSFID"] = parentProjectSFID log.WithFields(f).Debug("located parentProjectID...") log.WithFields(f).Debug("adding github organization...") - resp, err := s.repo.AddGithubOrganization(ctx, parentProjectSFID, projectSFID, &in) + resp, err := s.repo.AddGitHubOrganization(ctx, parentProjectSFID, projectSFID, &in) if err != nil { log.WithFields(f).WithError(err).Warn("problem adding github organization for project") return nil, err @@ -280,12 +341,12 @@ func (s service) AddGithubOrganization(ctx context.Context, projectSFID string, } func (s service) UpdateGithubOrganization(ctx context.Context, projectSFID string, organizationName string, autoEnabled bool, autoEnabledClaGroupID string, branchProtectionEnabled bool) error { - return s.repo.UpdateGithubOrganization(ctx, projectSFID, organizationName, autoEnabled, autoEnabledClaGroupID, branchProtectionEnabled) + return s.repo.UpdateGitHubOrganization(ctx, projectSFID, organizationName, autoEnabled, autoEnabledClaGroupID, branchProtectionEnabled, nil) } func (s service) DeleteGithubOrganization(ctx context.Context, projectSFID string, githubOrgName string) error { f := logrus.Fields{ - "functionName": "DeleteGitHubOrganization", + "functionName": "v2.github_organizations.service.DeleteGitHubOrganization", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": projectSFID, "githubOrgName": githubOrgName, @@ -299,13 +360,37 @@ func (s service) DeleteGithubOrganization(ctx context.Context, projectSFID strin return projectErr } + // check to see if ghorg to be deleted is fetched via parent or project sfid + // if it is fetched via parent sfid then we need to get the parent project sfid + // to delete the ghorg + pcg, pcgErr := s.projectsCLAGroupService.GetClaGroupIDForProject(ctx, projectSFID) + if pcgErr != nil { + if pcgErr == projects_cla_groups.ErrProjectNotAssociatedWithClaGroup { + log.WithFields(f).Warnf("unable to locate project CLA Group mapping for project SFID: %s, error: %+v", projectSFID, pcgErr) + } else { + log.WithFields(f).WithError(pcgErr).Warnf("unable to load project CLA group for project SFID: %s", projectSFID) + return pcgErr + } + } + + if pcg != nil && pcg.FoundationSFID != "" { + log.WithFields(f).Debug("disabling repositories for github organization...") + err := s.gitV1Repository.GitHubDisableRepositoriesOfOrganizationParent(ctx, pcg.FoundationSFID, githubOrgName) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem disabling repositories for github organization") + return err + } + log.WithFields(f).Debug("deleting github organization under foundation...") + return s.repo.DeleteGitHubOrganizationByParent(ctx, pcg.FoundationSFID, githubOrgName) + } + log.WithFields(f).Debug("disabling repositories for github organization...") - err := s.ghRepository.DisableRepositoriesOfGithubOrganization(ctx, projectSFID, githubOrgName) + err := s.gitV1Repository.GitHubDisableRepositoriesOfOrganization(ctx, projectSFID, githubOrgName) if err != nil { log.WithFields(f).WithError(err).Warn("problem disabling repositories for github organization") return err } - log.WithFields(f).Debug("deleting github github organization...") - return s.repo.DeleteGithubOrganization(ctx, projectSFID, githubOrgName) + log.WithFields(f).Debug("deleting github organization under project...") + return s.repo.DeleteGitHubOrganization(ctx, projectSFID, githubOrgName) } diff --git a/cla-backend-go/v2/gitlab-activity/handlers.go b/cla-backend-go/v2/gitlab-activity/handlers.go new file mode 100644 index 000000000..d3ec1bb4d --- /dev/null +++ b/cla-backend-go/v2/gitlab-activity/handlers.go @@ -0,0 +1,295 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab_activity + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + + gitlab_api "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" + "github.com/communitybridge/easycla/cla-backend-go/v2/gitlab_organizations" + "github.com/communitybridge/easycla/cla-backend-go/v2/gitlab_sign" + + "github.com/communitybridge/easycla/cla-backend-go/events" + "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations" + "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations/gitlab_activity" + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/gofrs/uuid" + "github.com/savaki/dynastore" + "github.com/sirupsen/logrus" + gitlabsdk "github.com/xanzy/go-gitlab" +) + +const ( + // SessionStoreKey for cla-gitlab + SessionStoreKey = "cla-gitlab" +) + +func Configure(api *operations.EasyclaAPI, service Service, gitlabOrgService gitlab_organizations.ServiceInterface, eventService events.Service, gitLabApp *gitlab_api.App, signService gitlab_sign.Service, contributorConsoleV2Base string, sessionStore *dynastore.Store) { + + api.GitlabActivityGitlabTriggerHandler = gitlab_activity.GitlabTriggerHandlerFunc(func(params gitlab_activity.GitlabTriggerParams) middleware.Responder { + requestID, _ := uuid.NewV4() + reqID := requestID.String() + + if params.GitlabTriggerInput == nil || params.GitlabTriggerInput.GitlabOrganizationID == nil || params.GitlabTriggerInput.GitlabExternalRepositoryID == nil || params.GitlabTriggerInput.GitlabMrID == nil { + return gitlab_activity.NewGitlabActivityBadRequest().WithPayload( + utils.ErrorResponseBadRequest(reqID, "missing parameter")) + } + + gitlabOrganizationID := *params.GitlabTriggerInput.GitlabOrganizationID + gitlabExternalRepositoryID := *params.GitlabTriggerInput.GitlabExternalRepositoryID + gitlabMrID := *params.GitlabTriggerInput.GitlabMrID + + f := logrus.Fields{ + "functionName": "gitlab_activity.handlers.GitlabActivityGitlabTriggerHandler", + "requestID": reqID, + "gitlabOrganizationID": gitlabOrganizationID, + "gitlabExternalRepositoryID": gitlabExternalRepositoryID, + "gitlabMrID": gitlabMrID, + } + + log.WithFields(f).Debugf("handling gitlab trigger") + ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) + + gitlabOrg, err := gitlabOrgService.GetGitLabOrganizationByID(ctx, gitlabOrganizationID) + if err != nil { + msg := fmt.Sprintf("fetching gitlab org failed : %v", err) + log.WithFields(f).Errorf(msg) + return gitlab_activity.NewGitlabActivityBadRequest().WithPayload( + utils.ErrorResponseBadRequest(reqID, msg)) + } + + if gitlabOrg == nil { + msg := fmt.Sprintf("fetching gitlab org failed no results returned") + log.WithFields(f).Errorf(msg) + return gitlab_activity.NewGitlabActivityBadRequest().WithPayload( + utils.ErrorResponseBadRequest(reqID, msg)) + } + + encryptedOauthResponse, err := gitlabOrgService.RefreshGitLabOrganizationAuth(ctx, gitlabOrg) + if err != nil { + msg := fmt.Sprintf("refreshing gitlab org auth failed : %v", err) + log.WithFields(f).Errorf(msg) + return gitlab_activity.NewGitlabActivityBadRequest().WithPayload( + utils.ErrorResponseBadRequest(reqID, msg)) + } + + gitlabClient, err := gitlab_api.NewGitlabOauthClient(*encryptedOauthResponse, gitLabApp) + if err != nil { + msg := fmt.Sprintf("initializing gitlab client : %v", err) + log.WithFields(f).Errorf(msg) + return gitlab_activity.NewGitlabActivityBadRequest().WithPayload( + utils.ErrorResponseBadRequest(reqID, msg)) + } + + log.WithFields(f).Debugf("fetching gitlab repository via external id") + gitlabProject, err := gitlab_api.GetProjectByID(ctx, gitlabClient, int(gitlabExternalRepositoryID)) + if err != nil { + msg := fmt.Sprintf("fetching gitlab project failed : %v", err) + log.WithFields(f).Errorf(msg) + return gitlab_activity.NewGitlabActivityBadRequest().WithPayload( + utils.ErrorResponseBadRequest(reqID, msg)) + } + + gitlabMr, err := gitlab_api.FetchMrInfo(gitlabClient, int(gitlabExternalRepositoryID), int(gitlabMrID)) + if err != nil { + msg := fmt.Sprintf("fetching gitlab mr failed : %v", err) + log.WithFields(f).Errorf(msg) + return gitlab_activity.NewGitlabActivityBadRequest().WithPayload( + utils.ErrorResponseBadRequest(reqID, msg)) + } + + err = service.ProcessMergeActivity(ctx, gitlabOrg.AuthState, &ProcessMergeActivityInput{ + ProjectName: gitlabProject.Name, + ProjectPath: gitlabProject.PathWithNamespace, + ProjectNamespace: gitlabProject.Namespace.Name, + ProjectID: gitlabProject.ID, + MergeID: int(gitlabMrID), + RepositoryPath: gitlabProject.PathWithNamespace, + LastCommitSha: gitlabMr.SHA, + }) + if err != nil { + msg := fmt.Sprintf("processing gitlab merge event failed : %v", err) + log.WithFields(f).Errorf(msg) + if errors.Is(err, secretTokenMismatch) { + return gitlab_activity.NewGitlabActivityUnauthorized().WithPayload( + utils.ErrorResponseUnauthorized(reqID, msg)) + } + return gitlab_activity.NewGitlabActivityInternalServerError().WithPayload( + utils.ErrorResponseBadRequest(reqID, msg)) + } + + return gitlab_activity.NewGitlabActivityOK() + + }) + + api.GitlabActivityGitlabActivityHandler = gitlab_activity.GitlabActivityHandlerFunc(func(params gitlab_activity.GitlabActivityParams) middleware.Responder { + requestID, _ := uuid.NewV4() + reqID := requestID.String() + f := logrus.Fields{ + "functionName": "gitlab_activity.handlers.GitlabActivityGitlabActivityHandler", + "requestID": reqID, + } + log.WithFields(f).Debugf("handling gitlab activity callback") + ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) + + // if params.XGitlabToken == "" { + // return gitlab_activity.NewGitlabActivityUnauthorized().WithPayload( + // utils.ErrorResponseUnauthorized(reqID, "missing webhook secret token")) + // } + + // General note for this API endpoint: + // Even though we had an issue - we will a 200 request indicating that we received the event, otherwise + // gitlab will disable the webhook after several failed requests (need to confirm this behavior) + // + // From GitLab: + // Webhooks that return response codes in the 5xx range are understood to be failing intermittently and are temporarily disabled. These webhooks are initially disabled for 1 minute, which is extended on each retry up to a maximum of 24 hours. + // Webhooks that return response codes in the 4xx range are understood to be misconfigured and are permanently disabled until you manually re-enable them yourself. + // See Troubleshooting https://docs.gitlab.com/ee/user/project/integrations/webhooks.html#troubleshoot-webhooks for more information on disabled webhooks and how to re-enable them. + + jsonData, err := params.GitlabActivityInput.MarshalJSON() + if err != nil { + msg := fmt.Sprintf("unmarshall event data failed : %v", err) + log.WithFields(f).Debugf(msg) + // Always return 200 response + return gitlab_activity.NewGitlabActivityOK() + } + + event, err := gitlabsdk.ParseWebhook(gitlabsdk.EventTypeMergeRequest, jsonData) + if err != nil { + msg := fmt.Sprintf("parsing gitlab merge event type failed : %v", err) + log.WithFields(f).Debugf(msg) + // Always return 200 response + return gitlab_activity.NewGitlabActivityOK() + } + + mergeEvent, ok := event.(*gitlabsdk.MergeEvent) + if !ok { + msg := fmt.Sprintf("parsing gitlab merge event typecast failed : %v", err) + log.WithFields(f).Debugf(msg) + // Always return 200 response + return gitlab_activity.NewGitlabActivityOK() + } + + if mergeEvent.ObjectKind == "merge_request" { + + if mergeEvent.ObjectAttributes.State != "opened" && mergeEvent.ObjectAttributes.State != "update" && mergeEvent.ObjectAttributes.State != "reopen" { + msg := fmt.Sprintf("parsing gitlab merge event : %s failed, only [open, update, reopen] accepted", mergeEvent.ObjectAttributes.State) + log.WithFields(f).Debugf(msg) + // Always return 200 response + return gitlab_activity.NewGitlabActivityOK() + } + + err = service.ProcessMergeOpenedActivity(ctx, params.XGitlabToken, mergeEvent) + if err != nil { + msg := fmt.Sprintf("processing gitlab merge event failed : %v", err) + log.WithFields(f).Debugf(msg) + // Always return 200 response + return gitlab_activity.NewGitlabActivityOK() + } + + } else if mergeEvent.ObjectKind == "note" && strings.Contains(mergeEvent.ObjectAttributes.Description, "/easycla") { + log.WithFields(f).Debugf("processing gitlab merge comment event") + err = service.ProcessMergeCommentActivity(ctx, params.XGitlabToken, mergeEvent) + if err != nil { + msg := fmt.Sprintf("processing gitlab merge comment event failed : %v", err) + log.WithFields(f).Debugf(msg) + // Always return 200 response + return gitlab_activity.NewGitlabActivityOK() + } + } + + return gitlab_activity.NewGitlabActivityOK() + }) + + api.GitlabActivityGitlabUserOauthCallbackHandler = gitlab_activity.GitlabUserOauthCallbackHandlerFunc( + func(guocp gitlab_activity.GitlabUserOauthCallbackParams) middleware.Responder { + reqID := utils.GetRequestID(guocp.XREQUESTID) + ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) + f := logrus.Fields{ + "functionName": "gitlab_activity.handler.GitlabActivityGitlabUserOauthCallbackHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "code": guocp.Code, + "state": guocp.State, + } + + return middleware.ResponderFunc( + func(rw http.ResponseWriter, p runtime.Producer) { + session, err := sessionStore.Get(guocp.HTTPRequest, SessionStoreKey) + if err != nil { + log.WithFields(f).WithError(err).Warn("error with session store lookup") + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + state, ok := session.Values["gitlab_oauth2_state"].(string) + if !ok { + log.WithFields(f).Warn("Error getting session state - missing from session object") + http.Error(rw, "no session state", http.StatusInternalServerError) + return + } + + gitlabOriginURL, ok := session.Values["gitlab_origin_url"].(string) + if !ok { + log.WithFields(f).Warn("Error getting gitlab_origin_url - missing from session object") + http.Error(rw, "no return url", http.StatusInternalServerError) + return + } + + repositoryID, ok := session.Values["gitab_repository_id"].(string) + if !ok { + log.WithFields(f).Warn("Error getting gitlab_repository_id - missing from session object") + http.Error(rw, "no return url", http.StatusInternalServerError) + return + } + + mergeRequestID, ok := session.Values["gitlab_merge_request_id"].(string) + if !ok { + log.WithFields(f).Warn("Error getting gitlab_merge_request_id - missing from session object") + http.Error(rw, "no return url", http.StatusInternalServerError) + return + } + + if guocp.State != state { + msg := fmt.Sprintf("mismatch state, received: %s from callback, but loaded our state as: %s", + guocp.State, state) + log.WithFields(f).Warn(msg) + http.Error(rw, msg, http.StatusInternalServerError) + return + } + + log.WithFields(f).Debug("Fetching access token for user...") + token, err := gitlab_api.FetchOauthCredentials(guocp.Code) + if err != nil { + msg := fmt.Sprint("unable to fetch access token for user") + log.WithFields(f).Warn(msg) + http.Error(rw, msg, http.StatusInternalServerError) + return + } + + session.Values["gitlab_oauth2_token"] = token.AccessToken + session.Save(guocp.HTTPRequest, rw) + + // Get client + gitlabClient, err := gitlab_api.NewGitlabOauthClientFromAccessToken(token.AccessToken) + if err != nil { + msg := fmt.Sprintf("unable to create gitlab client from token : %s ", token.AccessToken) + log.WithFields(f).Warn(msg) + http.Error(rw, msg, http.StatusInternalServerError) + return + } + + consoleURL, err := signService.InitiateSignRequest(ctx, guocp.HTTPRequest, gitlabClient, repositoryID, mergeRequestID, gitlabOriginURL, contributorConsoleV2Base, eventService) + log.WithFields(f).Debugf("redirecting to :%s ", *consoleURL) + http.Redirect(rw, guocp.HTTPRequest, *consoleURL, http.StatusSeeOther) + }) + }) + +} diff --git a/cla-backend-go/v2/gitlab-activity/service.go b/cla-backend-go/v2/gitlab-activity/service.go new file mode 100644 index 000000000..6d58e0237 --- /dev/null +++ b/cla-backend-go/v2/gitlab-activity/service.go @@ -0,0 +1,759 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab_activity + +import ( + "context" + "errors" + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/communitybridge/easycla/cla-backend-go/config" + + "github.com/communitybridge/easycla/cla-backend-go/company" + signatures1 "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/signatures" + + "github.com/aws/aws-sdk-go/aws" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + v2Models "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" + gitlab_api "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" + "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" + "github.com/communitybridge/easycla/cla-backend-go/repositories" + "github.com/communitybridge/easycla/cla-backend-go/signatures" + "github.com/communitybridge/easycla/cla-backend-go/users" + "github.com/communitybridge/easycla/cla-backend-go/v2/common" + "github.com/communitybridge/easycla/cla-backend-go/v2/gitlab_organizations" + gitV2Repositories "github.com/communitybridge/easycla/cla-backend-go/v2/repositories" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/sirupsen/logrus" + "github.com/xanzy/go-gitlab" +) + +var ( + missingID = errors.New("user missing in easyCLA records") + missingCompanyAffiliation = errors.New("must confirm affiliation with their company") + missingCompanyApproval = errors.New("missing in company approval lists") + secretTokenMismatch = errors.New("secret token mismatch") +) + +// ProcessMergeActivityInput is used to pass the data needed to trigger a gitlab mr check +type ProcessMergeActivityInput struct { + ProjectName string + ProjectPath string + ProjectNamespace string + ProjectID int + MergeID int + RepositoryPath string + LastCommitSha string +} + +type gatedGitlabUser struct { + *gitlab.User + err error +} + +type Service interface { + ProcessMergeCommentActivity(ctx context.Context, secretToken string, commentEvent *gitlab.MergeEvent) error + ProcessMergeOpenedActivity(ctx context.Context, secretToken string, mergeEvent *gitlab.MergeEvent) error + ProcessMergeActivity(ctx context.Context, secretToken string, input *ProcessMergeActivityInput) error + IsUserApprovedForSignature(ctx context.Context, f logrus.Fields, corporateSignature *models.Signature, user *models.User, gitlabUser *gitlab.User) bool +} + +type service struct { + usersRepository users.UserRepository + gitlabOrgService gitlab_organizations.ServiceInterface + gitRepository repositories.RepositoryInterface + gitV2Repository gitV2Repositories.RepositoryInterface + signaturesRepository signatures.SignatureRepository + projectsCLAGroupsRepository projects_cla_groups.Repository + companyRepository company.IRepository + signatureRepository signatures.SignatureRepository + gitLabApp *gitlab_api.App +} + +func NewService(gitRepository repositories.RepositoryInterface, gitV2Repository gitV2Repositories.RepositoryInterface, usersRepository users.UserRepository, signaturesRepository signatures.SignatureRepository, projectsCLAGroupsRepository projects_cla_groups.Repository, + companyRepository company.IRepository, signatureRepository signatures.SignatureRepository, gitlabOrgService gitlab_organizations.ServiceInterface) Service { + return &service{ + gitRepository: gitRepository, + gitV2Repository: gitV2Repository, + usersRepository: usersRepository, + signaturesRepository: signaturesRepository, + projectsCLAGroupsRepository: projectsCLAGroupsRepository, + companyRepository: companyRepository, + signatureRepository: signatureRepository, + gitLabApp: gitlab_api.Init(config.GetConfig().Gitlab.AppClientID, config.GetConfig().Gitlab.AppClientSecret, config.GetConfig().Gitlab.AppPrivateKey), + gitlabOrgService: gitlabOrgService, + } +} + +func (s *service) ProcessMergeOpenedActivity(ctx context.Context, secretToken string, mergeEvent *gitlab.MergeEvent) error { + projectName := mergeEvent.Project.Name + projectPath := mergeEvent.Project.PathWithNamespace + projectNamespace := mergeEvent.Project.Namespace + projectID := mergeEvent.Project.ID + mergeID := mergeEvent.ObjectAttributes.IID + repositoryPath := mergeEvent.Project.PathWithNamespace + lastCommitSha := mergeEvent.ObjectAttributes.LastCommit.ID + + input := &ProcessMergeActivityInput{ + ProjectName: projectName, + ProjectPath: projectPath, + ProjectNamespace: projectNamespace, + ProjectID: projectID, + MergeID: mergeID, + RepositoryPath: repositoryPath, + LastCommitSha: lastCommitSha, + } + + return s.ProcessMergeActivity(ctx, secretToken, input) + +} + +func (s *service) ProcessMergeCommentActivity(ctx context.Context, secretToken string, commentEvent *gitlab.MergeEvent) error { + f := logrus.Fields{ + "functionName": "ProcessMergeCommentActivity", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitlabProjectPath": commentEvent.Project.PathWithNamespace, + "projectID": commentEvent.Project.ID, + "projectPath": commentEvent.Project.PathWithNamespace, + "projectName": commentEvent.Project.Name, + "repositoryPath": commentEvent.Project.PathWithNamespace, + "commitSha": commentEvent.ObjectAttributes.LastCommit.ID, + } + + // Since we cant fetch the mergeID for comment event, we need to parse it from the URL + urlPathList := strings.Split(commentEvent.ObjectAttributes.URL, "/") + mergeID := strings.Split(urlPathList[len(urlPathList)-1], "#")[0] + if mergeID == "" { + return fmt.Errorf("merge ID not found in URL: %s", commentEvent.ObjectAttributes.URL) + } + mergeIDInt, err := strconv.Atoi(mergeID) + if err != nil { + return fmt.Errorf("unable to convert merge ID to int: %s, error: %v", mergeID, err) + } + + f["mergeID"] = mergeIDInt + + projectName := commentEvent.Project.Name + projectPath := commentEvent.Project.PathWithNamespace + projectNamespace := commentEvent.Project.Namespace + projectID := commentEvent.Project.ID + repositoryPath := commentEvent.Project.PathWithNamespace + + input := &ProcessMergeActivityInput{ + ProjectName: projectName, + ProjectPath: projectPath, + ProjectNamespace: projectNamespace, + ProjectID: projectID, + MergeID: mergeIDInt, + RepositoryPath: repositoryPath, + LastCommitSha: commentEvent.ObjectAttributes.LastCommit.ID, + } + + return s.ProcessMergeActivity(ctx, secretToken, input) +} + +func (s *service) ProcessMergeActivity(ctx context.Context, secretToken string, input *ProcessMergeActivityInput) error { + projectName := input.ProjectName + projectPath := input.ProjectPath + projectNamespace := input.ProjectNamespace + projectID := input.ProjectID + mergeID := input.MergeID + repositoryPath := input.RepositoryPath + lastCommitSha := input.LastCommitSha + + f := logrus.Fields{ + "functionName": "ProcessMergeActivity", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitlabProjectPath": projectPath, + "gitlabProjectName": projectName, + "gitlabProjectID": projectID, + "gitlabProjectNamespace": projectNamespace, + "mergeID": mergeID, + "repositoryName": repositoryPath, + } + + log.WithFields(f).Debugf("looking up for gitlab org in easycla records ...") + gitlabOrg, err := s.getGitlabOrganizationFromProjectPath(ctx, projectPath, projectNamespace) + if err != nil { + return fmt.Errorf("fetching internal gitlab org for following path : %s failed : %v", repositoryPath, err) + } + + // log.WithFields(f).Debugf("checking gitlab org : %s auth state agains the webhook secret token", gitlabOrg.OrganizationName) + // if gitlabOrg.AuthState != secretToken { + // return secretTokenMismatch + // } + + log.WithFields(f).Debugf("internal gitlab org : %s:%s is associated with external path : %s", gitlabOrg.OrganizationID, gitlabOrg.OrganizationName, repositoryPath) + + // fetch updated token info + log.WithFields(f).Debugf("refreshing gitlab org : %s:%s auth info", gitlabOrg.OrganizationID, gitlabOrg.OrganizationName) + oauthResponse, err := s.gitlabOrgService.RefreshGitLabOrganizationAuth(ctx, common.ToCommonModel(gitlabOrg)) + if err != nil { + return fmt.Errorf("refreshing gitlab org auth info failed : %v", err) + } + + gitlabClient, err := gitlab_api.NewGitlabOauthClient(*oauthResponse, s.gitLabApp) + if err != nil { + return fmt.Errorf("initializing gitlab client : %v", err) + } + + if lastCommitSha == "" { + log.WithFields(f).Debugf("loading GitLab merge request info for merge request: %d", mergeID) + lastSha, err := gitlab_api.GetLatestCommit(gitlabClient, projectID, mergeID) + if err != nil { + return fmt.Errorf("fetching info for mr : %d and project : %d: %s, failed : %v", mergeID, projectID, projectName, err) + } + lastCommitSha = lastSha.ID + } + + f["lastCommitSha"] = lastCommitSha + log.WithFields(f).Debugf("last commit sha for merge request: %d is %s", mergeID, lastCommitSha) + + _, err = gitlab_api.FetchMrInfo(gitlabClient, projectID, mergeID) + if err != nil { + return fmt.Errorf("fetching info for mr : %d and project : %d: %s, failed : %v", mergeID, projectID, projectName, err) + } + + // try to find the repository via the external id + gitlabRepo, err := s.getGitlabRepoByName(ctx, repositoryPath) + if err != nil { + return fmt.Errorf("finding internal repository for gitlab org name failed : %v", err) + } + + log.WithFields(f).Debugf("loading GitLab merge request participatants for merge request: %d", mergeID) + participants, err := gitlab_api.FetchMrParticipants(gitlabClient, projectID, mergeID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem loading GitLab merge request participants for merge request: %d", mergeID) + return fmt.Errorf("problem loading GitLab merge request participants for merge request: %d - error: %+v", mergeID, err) + } + + if len(participants) == 0 { + return fmt.Errorf("no participants found in GitLab mr : %d, and gitlab project : %d", mergeID, projectID) + } + + claGroup, err := s.projectsCLAGroupsRepository.GetClaGroupIDForProject(ctx, gitlabOrg.ProjectSfid) + if err != nil { + return fmt.Errorf("fetching claGroup id for gitlabOrg project sfid : %s, failed : %v", gitlabOrg.ProjectSfid, err) + } + claGroupID := claGroup.ClaGroupID + log.WithFields(f).Debugf("gitlabOrg : %s is associated with cla group id : %s", gitlabOrg.OrganizationName, claGroupID) + + log.WithFields(f).Debugf("found %d participants for the MR ", len(participants)) + missingCLAMsg := "Missing CLA Authorization" + signedCLAMsg := "EasyCLA check passed. You are authorized to contribute." + + var missingUsers []*gatedGitlabUser + var signedUsers []*gitlab.User + for _, gitlabUser := range participants { + log.WithFields(f).Debugf("checking if GitLab user: %s (%d) with email: %s has signed", gitlabUser.Username, gitlabUser.ID, gitlabUser.Email) + userSigned, signedCheckErr := s.hasUserSigned(ctx, claGroupID, gitlabUser) + if signedCheckErr != nil { + log.WithFields(f).WithError(signedCheckErr).Warnf("problem checking if user : %s (%d) has signed - assuming not signed", gitlabUser.Username, gitlabUser.ID) + missingUsers = append(missingUsers, &gatedGitlabUser{ + User: gitlabUser, + err: err, + }) + continue + } + + if userSigned { + log.WithFields(f).Infof("gitlabUser: %s (%d) has signed", gitlabUser.Username, gitlabUser.ID) + signedUsers = append(signedUsers, gitlabUser) + } else { + log.WithFields(f).Infof("gitlabUser: %s (%d) has NOT signed", gitlabUser.Username, gitlabUser.ID) + missingUsers = append(missingUsers, &gatedGitlabUser{ + User: gitlabUser, + err: err, + }) + } + } + + signURL := GetFullSignURL(gitlabOrg.OrganizationID, strconv.Itoa(int(gitlabRepo.RepositoryExternalID)), strconv.Itoa(mergeID)) + mrCommentContent := PrepareMrCommentContent(missingUsers, signedUsers, signURL) + if len(missingUsers) > 0 { + log.WithFields(f).Errorf("merge request faild with 1 or more users not passing authorization - failed users : %+v", missingUsers) + if statusErr := gitlab_api.SetCommitStatus(gitlabClient, projectID, lastCommitSha, gitlab.Failed, missingCLAMsg, signURL); statusErr != nil { + log.WithFields(f).WithError(statusErr).Warnf("problem setting the commit status for merge request ID: %d, sha: %s", mergeID, lastCommitSha) + return fmt.Errorf("setting commit status failed : %v", statusErr) + } + + if mrCommentErr := gitlab_api.SetMrComment(gitlabClient, projectID, mergeID, mrCommentContent); mrCommentErr != nil { + log.WithFields(f).WithError(mrCommentErr).Warnf("problem setting the commit merge request comment for merge request ID: %d", mergeID) + return fmt.Errorf("setting comment failed : %v", mrCommentErr) + } + + return nil + } + + commitStatusErr := gitlab_api.SetCommitStatus(gitlabClient, projectID, lastCommitSha, gitlab.Success, signedCLAMsg, "") + if commitStatusErr != nil { + log.WithFields(f).WithError(commitStatusErr).Warnf("problem setting the commit status for merge request ID: %d, sha: %s", mergeID, lastCommitSha) + return fmt.Errorf("setting commit status failed : %v", commitStatusErr) + } + + if mrCommentErr := gitlab_api.SetMrComment(gitlabClient, projectID, mergeID, mrCommentContent); mrCommentErr != nil { + log.WithFields(f).WithError(mrCommentErr).Warnf("problem setting the commit merge request comment for merge request ID: %d", mergeID) + return fmt.Errorf("setting comment failed : %v", mrCommentErr) + } + + return nil +} + +func PrepareMrCommentContent(missingUsers []*gatedGitlabUser, signedUsers []*gitlab.User, signURL string) string { + landingPage := config.GetConfig().CLALandingPage + landingPage += "/#/?version=2" + + var badgeHyperlink string + if len(missingUsers) > 0 { + badgeHyperlink = signURL + } else { + badgeHyperlink = landingPage + } + + coveredBadge := fmt.Sprintf(` +Thank you for installing the LFX EasyCLA GitLab Application/Bot. Your GitLab Group and repositories are now onboarded.
+To review the configuration or revoke the application, navigate to the GitLab Applications under your User Settings.
+You may now close this window and return to the LFX Project Control Center and select the repositories for EasyCLA.
+ + `, configPage) + + rw.Header().Set("Content-Type", "text/html") + rw.Header().Set(utils.XREQUESTID, o.ReqID) + rw.WriteHeader(http.StatusOK) + _, err := rw.Write([]byte(html)) + if err != nil { + panic(err) + } +} + +// ServerError Success +type ServerError struct { + ReqID string + GitLabGroupName string + Error error +} + +// NewServerError creates a new redirect handler +func NewServerError(reqID string, gitLabGroupName string, theError error) *ServerError { + return &ServerError{ + ReqID: reqID, + GitLabGroupName: gitLabGroupName, + Error: theError, + } +} + +// WriteResponse to the client +func (o *ServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + html := fmt.Sprintf(` + + +Unable to install the GitLab Group %s due to the following error: %s.
+ + `, o.GitLabGroupName, o.Error.Error()) + + rw.Header().Set("Content-Type", "text/html") + rw.Header().Set(utils.XREQUESTID, o.ReqID) + _, err := rw.Write([]byte(html)) + if err != nil { + panic(err) + } +} + +// cleanPath helper function that strips the groups/ prefix in full path +func cleanPath(fullPath string) string { + f := logrus.Fields{ + "functionName": "gitlab_organizations.handlers.cleanPath", + "orgPathName": fullPath, + } + var result string + result = fullPath + reg := `(?PHello %s,
", params.SignatoryName) + emailBody += fmt.Sprintf("
This is a notification email from EasyCLA regarding the project(s) %s associated with the CLA Group %s. %s has designated you as an authorized signatory for the organization %s. In order for employees of your company to contribute to any of the above project(s), they must do so under a Contributor License Agreement signed by someone with authority on behalf of your company.
", projectNamesList, params.ClaGroupName, params.ClaManagerName, params.CompanyName) + emailBody += fmt.Sprintf("After you sign, %s (as the initial CLA Manager for your company) will be able to maintain the list of specific employees authorized to contribute to the project(s) under this signed CLA.
", params.ClaManagerName) + emailBody += fmt.Sprintf("If you are authorized to sign on your company’s behalf, and if you approve %s as your initial CLA Manager, please review the document and sign the CLA. If you have questions, or if you are not an authorized signatory of this company, please contact the requester at %s.
", params.ClaManagerName, params.ClaManagerEmail) + // You would need to implement the appendEmailHelpSignOffContent function in Go separately + + return emailSubject, emailBody } diff --git a/cla-backend-go/v2/signatures/converters.go b/cla-backend-go/v2/signatures/converters.go index cef91da0d..39a6401de 100644 --- a/cla-backend-go/v2/signatures/converters.go +++ b/cla-backend-go/v2/signatures/converters.go @@ -6,12 +6,13 @@ package signatures import ( "fmt" "strings" + "sync" - v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/models" + v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/utils" - "github.com/go-openapi/strfmt" + "github.com/communitybridge/easycla/cla-backend-go/v2/approvals" "github.com/jinzhu/copier" "github.com/sirupsen/logrus" ) @@ -43,14 +44,138 @@ func v2SignaturesReplaceCompanyID(src *v1Models.Signatures, internalID, external // Replace the internal ID with the External ID for _, sig := range dst.Signatures { - if sig.SignatureReferenceID.String() == internalID { - sig.SignatureReferenceID = strfmt.UUID4(externalID) + if sig.SignatureReferenceID == internalID { + sig.SignatureReferenceID = externalID } } return &dst, nil } +func (s *Service) v2SignaturesToCorporateSignatures(src models.Signatures, projectSFID string) (*models.CorporateSignatures, error) { + f := logrus.Fields{ + "functionName": "v2SignaturesToCorporateSignatures", + "projectSFID": projectSFID, + } + + // Convert the signatures + log.WithFields(f).Debugf("converting %d signatures to corporate signatures", len(src.Signatures)) + + var dst models.CorporateSignatures + err := copier.Copy(&dst, src) + if err != nil { + return nil, err + } + + // Convert the individual signatures + for _, sigSrc := range src.Signatures { + for _, sigDest := range dst.Signatures { + err = s.TransformSignatureToCorporateSignature(sigSrc, sigDest, projectSFID) + if err != nil { + return nil, err + } + } + } + + return &dst, nil +} + +func searchSignatureApprovals(signatureID, criteria, name string, approvalList []approvals.ApprovalItem) []approvals.ApprovalItem { + f := logrus.Fields{ + "functionName": "searchSignatureApprovals", + "signatureID": signatureID, + "criteria": criteria, + "name": name, + } + + var result = make([]approvals.ApprovalItem, 0) + for _, approval := range approvalList { + if approval.SignatureID == signatureID && approval.ApprovalCriteria == criteria && approval.ApprovalName == name { + log.WithFields(f).Debugf("found approval for %s: %s :%s", criteria, name, approval.DateAdded) + result = append(result, approval) + } + } + + return result +} + +// TransformSignatureToCorporateSignature transforms a Signature model into a CorporateSignature model +func (s *Service) TransformSignatureToCorporateSignature(signature *models.Signature, corporateSignature *models.CorporateSignature, projectSFID string) error { + f := logrus.Fields{ + "functionName": "TransformSignatureToCorporateSignature", + "signatureID": signature.SignatureID, + } + + var wg sync.WaitGroup + var err error + + // fetch approval list items for signature + approvals, approvalErr := s.approvalsRepos.GetApprovalListBySignature(signature.SignatureID) + if approvalErr != nil { + log.WithFields(f).WithError(approvalErr).Warnf("unable to fetch approval list items for signature") + return approvalErr + } + + log.WithFields(f).Debugf("Fetched %d approval list items for signature", len(approvals)) + + signatureApprovals := map[string][]string{ + "domain": signature.DomainApprovalList, + "email": signature.EmailApprovalList, + "githubOrg": signature.GithubOrgApprovalList, + "githubUsername": signature.GithubUsernameApprovalList, + "gitlabOrg": signature.GitlabOrgApprovalList, + "gitlabUsername": signature.GitlabUsernameApprovalList, + } + + // Transform the approval list items + for key, value := range signatureApprovals { + wg.Add(1) + go func(key string, value []string) { + defer wg.Done() + for _, item := range value { + // Default to the signature modified date + // log.WithFields(f).Debugf("searching for approval for %s: %s", key, item) + approvalItem := models.ApprovalItem{ + ApprovalItem: item, + DateAdded: signature.SignatureModified, + } + foundApprovals := searchSignatureApprovals(signature.SignatureID, key, item, approvals) + + if len(foundApprovals) > 0 { + // ideally this should be one record + approvalItem.DateAdded = foundApprovals[0].DateAdded + log.WithFields(f).Debugf("found approval for %s: %s :%s", key, item, approvalItem.DateAdded) + } + + switch key { + case "domain": + corporateSignature.DomainApprovalList = append(corporateSignature.DomainApprovalList, &approvalItem) + case "email": + corporateSignature.EmailApprovalList = append(corporateSignature.EmailApprovalList, &approvalItem) + case "githubOrg": + corporateSignature.GithubOrgApprovalList = append(corporateSignature.GithubOrgApprovalList, &approvalItem) + case "githubUsername": + corporateSignature.GithubUsernameApprovalList = append(corporateSignature.GithubUsernameApprovalList, &approvalItem) + case "gitlabOrg": + corporateSignature.GitlabOrgApprovalList = append(corporateSignature.GitlabOrgApprovalList, &approvalItem) + case "gitlabUsername": + corporateSignature.GitlabUsernameApprovalList = append(corporateSignature.GitlabUsernameApprovalList, &approvalItem) + } + } + }(key, value) + + } + + log.WithFields(f).Debug("waiting for approval list items to be processed") + wg.Wait() + return err + +} + +func iclaSigCsvHeader() string { + return `Name,GitHub Username,GitLab Username,LF_ID,Email,Signed Date,Approved,Signed` +} + func iclaSigCsvLine(sig *v1Models.IclaSignature) string { var dateTime string t, err := utils.ParseDateTime(sig.SignedOn) @@ -60,11 +185,11 @@ func iclaSigCsvLine(sig *v1Models.IclaSignature) string { } else { dateTime = t.Format("Jan 2,2006") } - return fmt.Sprintf("\n%s,%s,%s,%s,\"%s\"", sig.GithubUsername, sig.LfUsername, sig.UserName, sig.UserEmail, dateTime) + return fmt.Sprintf("\n\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",%t,%t", sig.UserName, sig.GithubUsername, sig.GitlabUsername, sig.LfUsername, sig.UserEmail, dateTime, sig.SignatureApproved, sig.SignatureSigned) } func cclaSigCsvHeader() string { - return `Company Name,Signed,Approved,DomainApprovalList,EmailApprovalList,GitHubOrgApprovalList,GitHubUsernameApprovalList,Date Signed` + return `Company Name,Signed,Approved,DomainApprovalList,EmailApprovalList,GitHubOrgApprovalList,GitHubUsernameApprovalList,Date Signed,Approved,Signed` } func cclaSigCsvLine(sig *v1Models.Signature) string { @@ -76,7 +201,7 @@ func cclaSigCsvLine(sig *v1Models.Signature) string { } else { dateTime = t.Format("Jan 2,2006") } - return fmt.Sprintf("\n\"%s\",%t,%t,\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"", + return fmt.Sprintf("\n\"%s\",%t,%t,\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",%t,%t", sig.CompanyName, sig.SignatureSigned, sig.SignatureApproved, @@ -84,5 +209,7 @@ func cclaSigCsvLine(sig *v1Models.Signature) string { strings.Join(sig.EmailApprovalList, ","), strings.Join(sig.GithubOrgApprovalList, ","), strings.Join(sig.GithubUsernameApprovalList, ","), - dateTime) + dateTime, + sig.SignatureApproved, + sig.SignatureApproved) } diff --git a/cla-backend-go/v2/signatures/handlers.go b/cla-backend-go/v2/signatures/handlers.go index 1f8078e2b..8a7553d6a 100644 --- a/cla-backend-go/v2/signatures/handlers.go +++ b/cla-backend-go/v2/signatures/handlers.go @@ -9,26 +9,28 @@ import ( "fmt" "net/http" "strings" + "sync" + + "github.com/communitybridge/easycla/cla-backend-go/project/repository" + "github.com/communitybridge/easycla/cla-backend-go/project/service" "github.com/aws/aws-sdk-go/aws" "github.com/sirupsen/logrus" "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" - "github.com/communitybridge/easycla/cla-backend-go/v2/organization-service/client/organizations" + "github.com/communitybridge/easycla/cla-backend-go/v2/organization-service/client/organizations" // nolint - lint error for import not used, but it really is "github.com/go-openapi/runtime" - "github.com/communitybridge/easycla/cla-backend-go/project" - "github.com/communitybridge/easycla/cla-backend-go/company" - v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/models" + 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/LF-Engineering/lfx-kit/auth" "github.com/communitybridge/easycla/cla-backend-go/events" - v1Signatures "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/signatures" + v1Signatures "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/signatures" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations/signatures" "github.com/communitybridge/easycla/cla-backend-go/github" @@ -40,7 +42,7 @@ import ( ) // Configure setups handlers on api with service -func Configure(api *operations.EasyclaAPI, projectService project.Service, projectRepo project.ProjectRepository, companyService company.IService, v1SignatureService signatureService.SignatureService, sessionStore *dynastore.Store, eventsService events.Service, v2service Service, projectClaGroupsRepo projects_cla_groups.Repository) { //nolint +func Configure(api *operations.EasyclaAPI, claGroupService service.Service, projectRepo repository.ProjectRepository, companyService company.IService, v1SignatureService signatureService.SignatureService, sessionStore *dynastore.Store, eventsService events.Service, v2SignatureService ServiceInterface, projectClaGroupsRepo projects_cla_groups.Repository) { //nolint const problemLoadingCLAGroupByID = "problem loading cla group by ID" const iclaNotSupportedForCLAGroup = "individual contribution is not supported for this project" @@ -50,8 +52,9 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje api.SignaturesGetSignatureHandler = signatures.GetSignatureHandlerFunc(func(params signatures.GetSignatureParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "SignaturesGetGitHubOrgWhitelistHandler", + "functionName": "v2.signatures.handlers.SignaturesGetSignatureHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "signatureID": params.SignatureID, } @@ -95,17 +98,29 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "SignaturesUpdateApprovalListHandler", + "functionName": "v2.signatures.handlers.SignaturesUpdateApprovalListHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": params.ClaGroupID, "projectSFID": params.ProjectSFID, - "companySFID": params.CompanySFID, + "companyID": params.CompanyID, + } + + companyModel, err := companyService.GetCompany(ctx, params.CompanyID) + if err != nil { + msg := fmt.Sprintf("User lookup for company by ID: %s failed : %v", params.CompanyID, err) + log.Warn(msg) + if _, ok := err.(*utils.CompanyNotFound); ok { + return signatures.NewUpdateApprovalListBadRequest().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, fmt.Sprintf("company not found - unable to locate company by ID: %s", params.CompanyID), err)) + } + return signatures.NewUpdateApprovalListBadRequest().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, fmt.Sprintf("unable to locate company by ID: %s", params.CompanyID), err)) } // Must be in the Project|Organization Scope to see this - signature ACL is double-checked in the service level when the signature is loaded - if !utils.IsUserAuthorizedForProjectOrganizationTree(authUser, params.ProjectSFID, params.CompanySFID, utils.DISALLOW_ADMIN_SCOPE) { - msg := fmt.Sprintf("user %s does not have access to update Project Company Approval List with Project|Organization scope of %s | %s", - authUser.UserName, params.ProjectSFID, params.CompanySFID) + if !utils.IsUserAuthorizedForProjectOrganizationTree(ctx, authUser, params.ProjectSFID, companyModel.CompanyExternalID, utils.DISALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user '%s' does not have access to update Project Company Approval List with Project|Organization scope of %s | %s", + authUser.UserName, params.ProjectSFID, companyModel.CompanyExternalID) log.WithFields(f).Warn(msg) return signatures.NewUpdateApprovalListForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } @@ -118,25 +133,16 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje return validationError } - // Lookup the internal company ID when provided the external ID via the v1SignatureGService call - log.WithFields(f).Debug("loading company by company SFID") - companyModel, compErr := companyService.GetCompanyByExternalID(ctx, params.CompanySFID) - if compErr != nil || companyModel == nil { - msg := fmt.Sprintf("unable to locate company by external company ID: %s", params.CompanySFID) - log.WithFields(f).Warn(msg) - return signatures.NewUpdateApprovalListNotFound().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFound(reqID, msg)) - } - log.WithFields(f).Debug("loading CLA groups by projectSFID") - projectModels, projsErr := projectService.GetCLAGroupsByExternalSFID(ctx, params.ProjectSFID) - if projsErr != nil || projectModels == nil { + projectModels, projectErr := claGroupService.GetCLAGroupsByExternalSFID(ctx, params.ProjectSFID) + if projectErr != nil || projectModels == nil { msg := fmt.Sprintf("unable to locate projects by Project SFID: %s", params.ProjectSFID) log.WithFields(f).Warn(msg) return signatures.NewUpdateApprovalListNotFound().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFound(reqID, msg)) } // Lookup the internal project ID when provided the external ID via the v1SignatureService call - claGroupModel, projErr := projectService.GetCLAGroupByID(ctx, params.ClaGroupID) + claGroupModel, projErr := claGroupService.GetCLAGroupByID(ctx, params.ClaGroupID) if projErr != nil || claGroupModel == nil { msg := fmt.Sprintf("unable to locate project by CLA Group ID: %s", params.ClaGroupID) log.WithFields(f).Warn(msg) @@ -145,7 +151,7 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje // Convert the v2 input parameters to a v1 model v1ApprovalList := v1Models.ApprovalList{} - err := copier.Copy(&v1ApprovalList, params.Body) + err = copier.Copy(&v1ApprovalList, params.Body) if err != nil { msg := "unable to convert v1 to v2 approval list" log.WithFields(f).Warn(msg) @@ -154,14 +160,14 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje } // Invoke the update v1SignatureService function - updatedSig, updateErr := v1SignatureService.UpdateApprovalList(ctx, authUser, claGroupModel, companyModel, params.ClaGroupID, &v1ApprovalList) + updatedSig, updateErr := v1SignatureService.UpdateApprovalList(ctx, authUser, claGroupModel, companyModel, params.ClaGroupID, &v1ApprovalList, params.ProjectSFID) if updateErr != nil || updatedSig == nil { msg := fmt.Sprintf("unable to update signature approval list using CLA Group ID: %s", params.ClaGroupID) log.WithFields(f).Warn(msg) - if err, ok := err.(*signatureService.ForbiddenError); ok { - return signatures.NewUpdateApprovalListForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbiddenWithError(reqID, msg, err)) + if _, ok := err.(*signatureService.ForbiddenError); ok { + return signatures.NewUpdateApprovalListForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbiddenWithError(reqID, msg, updateErr)) } - return signatures.NewUpdateApprovalListBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequest(reqID, msg)) + return signatures.NewUpdateApprovalListBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, updateErr)) } // Convert the v1 output model to a v2 response model @@ -182,8 +188,9 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje api.SignaturesGetGitHubOrgWhitelistHandler = signatures.GetGitHubOrgWhitelistHandlerFunc(func(params signatures.GetGitHubOrgWhitelistParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "SignaturesGetGitHubOrgWhitelistHandler", + "functionName": "v2.signatures.handlers.SignaturesGetGitHubOrgWhitelistHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "signatureID": params.SignatureID, } @@ -199,15 +206,15 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje githubAccessToken = "" } - ghWhiteList, err := v1SignatureService.GetGithubOrganizationsFromWhitelist(ctx, params.SignatureID, githubAccessToken) + ghOrgApprovalList, err := v1SignatureService.GetGithubOrganizationsFromApprovalList(ctx, params.SignatureID, githubAccessToken) if err != nil { - log.WithFields(f).Warnf("error fetching github organization whitelist entries v using signature_id: %s, error: %+v", + log.WithFields(f).Warnf("error fetching github organization approval list entries using signature_id: %s, error: %+v", params.SignatureID, err) return signatures.NewGetGitHubOrgWhitelistBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) } var response []models.GithubOrg - err = copier.Copy(&response, ghWhiteList) + err = copier.Copy(&response, ghOrgApprovalList) if err != nil { return signatures.NewGetGitHubOrgWhitelistBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) } @@ -219,8 +226,9 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje api.SignaturesAddGitHubOrgWhitelistHandler = signatures.AddGitHubOrgWhitelistHandlerFunc(func(params signatures.AddGitHubOrgWhitelistParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "SignaturesAddGitHubOrgWhitelistHandler", + "functionName": "v2.signatures.handlers.SignaturesAddGitHubOrgWhitelistHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "signatureID": params.SignatureID, } @@ -244,7 +252,7 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje return signatures.NewAddGitHubOrgWhitelistBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) } - ghApprovalList, err := v1SignatureService.AddGithubOrganizationToWhitelist(ctx, params.SignatureID, input, githubAccessToken) + ghApprovalList, err := v1SignatureService.AddGithubOrganizationToApprovalList(ctx, params.SignatureID, input, githubAccessToken) if err != nil { log.WithFields(f).Warnf("error adding github organization %s using signature_id: %s to the approval list, error: %+v", *params.Body.OrganizationID, params.SignatureID, err) @@ -261,7 +269,7 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje } if signatureModel != nil { projectID = signatureModel.ProjectID - companyID = signatureModel.SignatureReferenceID.String() + companyID = signatureModel.SignatureReferenceID } eventsService.LogEvent(&events.LogEventArgs{ @@ -287,8 +295,9 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje api.SignaturesDeleteGitHubOrgWhitelistHandler = signatures.DeleteGitHubOrgWhitelistHandlerFunc(func(params signatures.DeleteGitHubOrgWhitelistParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "SignaturesDeleteGitHubOrgWhitelistHandler", + "functionName": "v2.signatures.handlers.SignaturesDeleteGitHubOrgWhitelistHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "signatureID": params.SignatureID, } @@ -311,7 +320,7 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje return signatures.NewDeleteGitHubOrgWhitelistBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) } - ghApprovalList, err := v1SignatureService.DeleteGithubOrganizationFromWhitelist(ctx, params.SignatureID, input, githubAccessToken) + ghApprovalList, err := v1SignatureService.DeleteGithubOrganizationFromApprovalList(ctx, params.SignatureID, input, githubAccessToken) if err != nil { log.WithFields(f).Warnf("error deleting github organization %s using signature_id: %s from the approval list, error: %+v", *params.Body.OrganizationID, params.SignatureID, err) @@ -328,7 +337,7 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje } if signatureModel != nil { projectID = signatureModel.ProjectID - companyID = signatureModel.SignatureReferenceID.String() + companyID = signatureModel.SignatureReferenceID } eventsService.LogEvent(&events.LogEventArgs{ EventType: events.ApprovalListGitHubOrganizationDeleted, @@ -352,18 +361,19 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje api.SignaturesGetProjectSignaturesHandler = signatures.GetProjectSignaturesHandlerFunc(func(params signatures.GetProjectSignaturesParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "SignaturesGetProjectSignaturesHandler", + "functionName": "v2.signatures.handlers.SignaturesGetProjectSignaturesHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": params.ClaGroupID, "signatureType": params.SignatureType, } log.WithFields(f).Debug("looking up CLA Group by ID...") - claGroupModel, err := projectService.GetCLAGroupByID(ctx, params.ClaGroupID) + claGroupModel, err := claGroupService.GetCLAGroupByID(ctx, params.ClaGroupID) if err != nil { log.WithFields(f).WithError(err).Warn(problemLoadingCLAGroupByID) - if err == project.ErrProjectDoesNotExist { + if err == repository.ErrProjectDoesNotExist { return signatures.NewGetProjectSignaturesNotFound().WithXRequestID(reqID).WithPayload( utils.ErrorResponseNotFoundWithError(reqID, problemLoadingCLAGroupByID, err)) } @@ -373,7 +383,7 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje f["foundationSFID"] = claGroupModel.FoundationSFID // Check to see if this CLA Group is configured for ICLAs... - if !claGroupModel.ProjectICLAEnabled { + if params.SignatureType != nil && utils.StringValue(params.ClaType) == utils.ClaTypeICLA && !claGroupModel.ProjectICLAEnabled { log.WithFields(f).Warn(iclaNotSupportedForCLAGroup) // Return 200 as the retool UI can't handle 400's return signatures.NewGetProjectSignaturesOK().WithXRequestID(reqID).WithPayload(&models.Signatures{ @@ -382,20 +392,28 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje Signatures: []*models.Signature{}, // empty list TotalCount: 0, }) - //return signatures.NewGetProjectSignaturesBadRequest().WithXRequestID(reqID).WithPayload( - // utils.ErrorResponseBadRequest(reqID, iclaNotSupportedForCLAGroup)) } - if false { - log.WithFields(f).Debug("checking access control permissions for user...") - if !isUserHaveAccessToCLAGroupProjects(ctx, authUser, params.ClaGroupID, projectClaGroupsRepo, projectRepo) { - msg := fmt.Sprintf("user %s is not authorized to view project ICLA signatures any scope of project", authUser.UserName) - log.Warn(msg) - return signatures.NewGetProjectSignaturesForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) - } - log.WithFields(f).Debug("user has access for this query") + // Check to see if this CLA Group is configured for CCLAs... + if params.SignatureType != nil && utils.StringValue(params.ClaType) == utils.ClaTypeCCLA && !claGroupModel.ProjectCCLAEnabled { + log.WithFields(f).Warn(cclaNotSupportedForCLAGroup) + // Return 200 as the retool UI can't handle 400's + return signatures.NewGetProjectSignaturesOK().WithXRequestID(reqID).WithPayload(&models.Signatures{ + ProjectID: params.ClaGroupID, + ResultCount: 0, + Signatures: []*models.Signature{}, // empty list + TotalCount: 0, + }) } + log.WithFields(f).Debug("checking access control permissions for user...") + if !isUserHaveAccessToCLAGroupProjects(ctx, authUser, params.ClaGroupID, projectClaGroupsRepo, projectRepo) { + msg := fmt.Sprintf("user '%s' is not authorized to view project ICLA signatures any scope of project", authUser.UserName) + log.Warn(msg) + return signatures.NewGetProjectSignaturesForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) + } + log.WithFields(f).Debug("user has access for this query") + log.WithFields(f).Debug("loading project signatures...") projectSignatures, err := v1SignatureService.GetProjectSignatures(ctx, v1Signatures.GetProjectSignaturesParams{ HTTPRequest: params.HTTPRequest, @@ -408,6 +426,8 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje SignatureType: params.SignatureType, ClaType: params.ClaType, SortOrder: params.SortOrder, + Approved: params.Approved, + Signed: params.Signed, }) if err != nil { msg := fmt.Sprintf("error retrieving project signatures for projectID: %s, error: %+v", @@ -431,29 +451,41 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje api.SignaturesGetProjectCompanySignaturesHandler = signatures.GetProjectCompanySignaturesHandlerFunc(func(params signatures.GetProjectCompanySignaturesParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "SignaturesGetProjectCompanySignaturesHandler", + "functionName": "v2.signatures.handlers.SignaturesGetProjectCompanySignaturesHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": params.ProjectSFID, - "companySFID": params.CompanySFID, + "companyID": params.CompanyID, } - // Must be in the one of the above scopes to see this - // - if project scope (like a PM) - // - if project|organization scope (like CLA Manager, CLA Signatory) - // - if organization scope (like company admin) - if !isUserHaveAccessToCLAProjectOrganization(ctx, authUser, params.ProjectSFID, params.CompanySFID, projectClaGroupsRepo) { + companyModel, err := companyService.GetCompany(ctx, params.CompanyID) + if err != nil { + msg := fmt.Sprintf("User lookup for company by ID: %s failed : %v", params.CompanyID, err) + log.Warn(msg) + if _, ok := err.(*utils.CompanyNotFound); ok { + return signatures.NewGetProjectCompanySignaturesBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ + Message: "EasyCLA - 404 Not Found - error getting company - " + msg, + Code: "404", + }) + } + return signatures.NewGetProjectCompanySignaturesBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ + Message: "EasyCLA - 400 Bad Request - error getting company - " + msg, + Code: "400", + }) + } + + if !isUserHaveAccessToCLAProjectOrganization(ctx, authUser, params.ProjectSFID, companyModel.CompanyExternalID, projectClaGroupsRepo) { msg := fmt.Sprintf("user %s is not authorized to view project company signatures any scope of project: %s, organization %s", - authUser.UserName, params.ProjectSFID, params.CompanySFID) + authUser.UserName, params.ProjectSFID, params.CompanyID) log.WithFields(f).Warn(msg) - return signatures.NewGetProjectCompanySignaturesForbidden().WithXRequestID(reqID).WithPayload( - utils.ErrorResponseForbidden(reqID, msg)) + return signatures.NewGetProjectCompanySignaturesForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } log.WithFields(f).Debug("loading project company signatures...") - projectSignatures, err := v2service.GetProjectCompanySignatures(ctx, params.CompanySFID, params.ProjectSFID) + projectSignatures, err := v2SignatureService.GetProjectCompanySignatures(ctx, params.CompanyID, companyModel.CompanyExternalID, params.ProjectSFID) if err != nil { - msg := fmt.Sprintf("error retrieving project signatures for project: %s, company: %s", params.ProjectSFID, params.CompanySFID) + msg := fmt.Sprintf("error retrieving project signatures for project: %s, company: %s", params.ProjectSFID, params.CompanyID) log.WithFields(f).Warn(msg) return signatures.NewGetProjectCompanySignaturesBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } @@ -466,67 +498,42 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje api.SignaturesGetProjectCompanyEmployeeSignaturesHandler = signatures.GetProjectCompanyEmployeeSignaturesHandlerFunc(func(params signatures.GetProjectCompanyEmployeeSignaturesParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "SignaturesGetProjectCompanyEmployeeSignaturesHandler", + "functionName": "v2.signatures.handlers.SignaturesGetProjectCompanyEmployeeSignaturesHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": params.ProjectSFID, - "companySFID": params.CompanySFID, + "companyID": params.CompanyID, "nextKey": aws.StringValue(params.NextKey), "pageSize": aws.Int64Value(params.PageSize), } - // Try to load the company model - use both approaches - internal and external - var companyModel *v1Models.Company - var err error - // Internal IDs are UUIDv4 - external are not - if utils.IsUUIDv4(params.CompanySFID) { - // Oops - not provided a SFID - but an internal ID - that'iclaNotSupported ok, we'll lookup via the internal ID - log.WithFields(f).Debug("companySFID provided as internal ID - looking up record by internal ID") - // Lookup the company model by internal ID - companyModel, err = companyService.GetCompany(ctx, params.CompanySFID) - if companyModel != nil && companyModel.CompanyExternalID == "" { - msg := fmt.Sprintf("problem loading company - company external ID not defined - comapny ID: %s", params.CompanySFID) - log.WithFields(f).WithError(err).Warn(msg) - return signatures.NewGetProjectCompanyEmployeeSignaturesBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFound( - reqID, msg)) - } - } else { - // Lookup the company model by external ID - log.WithFields(f).Debug("companySFID provided as external ID - looking up record by external ID") - companyModel, err = companyService.GetCompanyByExternalID(ctx, params.CompanySFID) - } + companyModel, err := companyService.GetCompany(ctx, params.CompanyID) if err != nil { - var companyDoesNotExistErr utils.CompanyDoesNotExist - if errors.Is(err, &companyDoesNotExistErr) { - msg := "problem loading company by ID" - log.WithFields(f).WithError(err).Warn(msg) - return signatures.NewGetProjectCompanyEmployeeSignaturesBadRequest().WithXRequestID(reqID).WithPayload( - utils.ErrorResponseNotFoundWithError(reqID, msg, err)) + msg := fmt.Sprintf("user lookup for company by ID: '%s' failed : %v", params.CompanyID, err) + log.Warn(msg) + if _, ok := err.(*utils.CompanyNotFound); ok { + return signatures.NewGetProjectCompanyEmployeeSignaturesBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequest(reqID, msg)) } - - log.WithFields(f).WithError(err).Warnf("problem loading company by ID") - return signatures.NewGetProjectCompanyEmployeeSignaturesBadRequest().WithXRequestID(reqID).WithPayload( - utils.ErrorResponseBadRequestWithError(reqID, fmt.Sprintf("problem loading company by ID: %s", params.CompanySFID), err)) + return signatures.NewGetProjectCompanyEmployeeSignaturesBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } if companyModel == nil { - msg := fmt.Sprintf("problem loading company by ID: %s", params.CompanySFID) + msg := fmt.Sprintf("problem loading company by ID: %s", params.CompanyID) log.WithFields(f).WithError(err).Warn(msg) - return signatures.NewGetProjectCompanyEmployeeSignaturesBadRequest().WithXRequestID(reqID).WithPayload( - utils.ErrorResponseNotFound(reqID, msg)) + return signatures.NewGetProjectCompanyEmployeeSignaturesBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFound(reqID, msg)) } log.WithFields(f).Debug("checking access control permissions...") - if !isUserHaveAccessToCLAProjectOrganization(ctx, authUser, params.ProjectSFID, params.CompanySFID, projectClaGroupsRepo) { - msg := fmt.Sprintf("user %s is not authorized to view project company signatures any scope of project: %s, organization %s", - authUser.UserName, params.ProjectSFID, params.CompanySFID) + if !isUserHaveAccessToCLAProjectOrganization(ctx, authUser, params.ProjectSFID, companyModel.CompanyExternalID, projectClaGroupsRepo) { + msg := fmt.Sprintf("user '%s' is not authorized to view project company signatures any scope of project or project|organization for project: '%s', organization '%s'", + authUser.UserName, params.ProjectSFID, params.CompanyID) log.Warn(msg) - return signatures.NewGetProjectCompanyEmployeeSignaturesForbidden().WithXRequestID(reqID).WithPayload( - utils.ErrorResponseForbidden(reqID, msg)) + return signatures.NewGetProjectCompanyEmployeeSignaturesForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } // Locate the CLA Group for the provided project SFID log.WithFields(f).Debug("loading project signatures...") - projectCLAGroupModel, err := projectClaGroupsRepo.GetClaGroupIDForProject(params.ProjectSFID) + projectCLAGroupModel, err := projectClaGroupsRepo.GetClaGroupIDForProject(ctx, params.ProjectSFID) if err != nil { log.WithFields(f).WithError(err).Warnf("problem loading project -> cla group mapping") return signatures.NewGetProjectCompanyEmployeeSignaturesBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError( @@ -545,12 +552,12 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje CompanyID: companyModel.CompanyID, // internal company id NextKey: params.NextKey, PageSize: params.PageSize, - }) + }, nil) if err != nil { log.WithFields(f).WithError(err).Warnf("error retrieving employee project signatures for project: %s, company: %s, error: %+v", - params.ProjectSFID, params.CompanySFID, err) + params.ProjectSFID, params.CompanyID, err) return signatures.NewGetProjectCompanyEmployeeSignaturesBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError( - reqID, fmt.Sprintf("unable to fetch employee signatures for project ID: %s and company: %s", params.ProjectSFID, params.CompanySFID), err)) + reqID, fmt.Sprintf("unable to fetch employee signatures for project ID: %s and company: %s", params.ProjectSFID, params.CompanyID), err)) } resp, err := v2Signatures(projectSignatures) @@ -571,29 +578,34 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "SignaturesGetCompanySignaturesHandler", + "functionName": "v2.signatures.handlers.SignaturesGetCompanySignaturesHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "companySFID": params.CompanySFID, + "companyID": params.CompanyID, "companyName": aws.StringValue(params.CompanyName), "signatureType": aws.StringValue(params.SignatureType), "nextKey": aws.StringValue(params.NextKey), "pageSize": aws.Int64Value(params.PageSize), } - // Lookup the internal company ID - companyModel, err := companyService.GetCompanyByExternalID(ctx, params.CompanySFID) + companyModel, err := companyService.GetCompany(ctx, params.CompanyID) if err != nil { - log.WithFields(f).WithError(err).Warnf("problem loading company by SFID - returning empty response") - // Not sure this is the correct response as the LFX UI/Admin console wants 200 empty lists instead of non-200 status back - return signatures.NewGetCompanySignaturesOK().WithXRequestID(reqID).WithPayload(&models.Signatures{ - Signatures: []*models.Signature{}, - ResultCount: 0, - TotalCount: 0, + msg := fmt.Sprintf("User lookup for company by ID: %s failed : %v", params.CompanyID, err) + log.Warn(msg) + if _, ok := err.(*utils.CompanyNotFound); ok { + return signatures.NewGetCompanySignaturesBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ + Message: "EasyCLA - 404 Not Found - error getting company - " + msg, + Code: "404", + }) + } + return signatures.NewGetCompanySignaturesBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ + Message: "EasyCLA - 400 Bad Request - error getting company - " + msg, + Code: "400", }) } + if companyModel == nil { log.WithFields(f).WithError(err).Warnf("problem loading company model by ID - returning empty response") - // Not sure this is the correct response as the LFX UI/Admin console wants 200 empty lists instead of non-200 status back + // the LFX UI/Admin console wants 200 empty lists instead of non-200 status back return signatures.NewGetCompanySignaturesOK().WithXRequestID(reqID).WithPayload(&models.Signatures{ Signatures: []*models.Signature{}, ResultCount: 0, @@ -601,7 +613,7 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje }) } - if !utils.IsUserAuthorizedForOrganization(authUser, companyModel.CompanyExternalID, utils.ALLOW_ADMIN_SCOPE) { + if !utils.IsUserAuthorizedForOrganization(ctx, authUser, companyModel.CompanyExternalID, utils.ALLOW_ADMIN_SCOPE) { msg := fmt.Sprintf("%s - user %s is not authorized to view company signatures with Organization scope: %s", utils.EasyCLA403Forbidden, authUser.UserName, companyModel.CompanyExternalID) log.WithFields(f).Warn(msg) @@ -626,7 +638,7 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } - // Nothing in the query response - return a empty model + // Nothing in the query response - return an empty model if companySignatures == nil || len(companySignatures.Signatures) == 0 { return signatures.NewGetCompanySignaturesOK().WithXRequestID(reqID).WithPayload(&models.Signatures{ Signatures: []*models.Signature{}, @@ -653,8 +665,9 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje api.SignaturesGetUserSignaturesHandler = signatures.GetUserSignaturesHandlerFunc(func(params signatures.GetUserSignaturesParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "SignaturesGetUserSignaturesHandler", + "functionName": "v2.signatures.handlers.SignaturesGetUserSignaturesHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "userID": params.UserID, "userName": aws.StringValue(params.UserName), @@ -668,7 +681,7 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje PageSize: params.PageSize, UserName: params.UserName, UserID: params.UserID, - }) + }, nil) if err != nil { msg := fmt.Sprintf("error retrieving user signatures for userID: %s", params.UserID) log.WithFields(f).WithError(err).Warn(msg) @@ -692,19 +705,20 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje api.SignaturesDownloadProjectSignatureEmployeeAsCSVHandler = signatures.DownloadProjectSignatureEmployeeAsCSVHandlerFunc(func(params signatures.DownloadProjectSignatureEmployeeAsCSVParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "SignaturesDownloadProjectSignatureEmployeeAsCSVHandler", + "functionName": "v2.signatures.handlers.SignaturesDownloadProjectSignatureEmployeeAsCSVHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": params.ClaGroupID, - "companySFID": params.CompanySFID, + "companyID": params.CompanyID, } log.WithFields(f).Debug("processing request...") log.WithFields(f).Debug("looking up CLA Group by ID...") - claGroupModel, err := projectService.GetCLAGroupByID(ctx, params.ClaGroupID) + claGroupModel, err := claGroupService.GetCLAGroupByID(ctx, params.ClaGroupID) if err != nil { log.WithFields(f).WithError(err).Warn(problemLoadingCLAGroupByID) - if err == project.ErrProjectDoesNotExist { + if err == repository.ErrProjectDoesNotExist { return signatures.NewDownloadProjectSignatureEmployeeAsCSVNotFound().WithXRequestID(reqID).WithPayload( utils.ErrorResponseNotFoundWithError(reqID, problemLoadingCLAGroupByID, err)) } @@ -731,19 +745,45 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje // utils.ErrorResponseBadRequest(reqID, cclaNotSupportedForCLAGroup)) } + companyModel, err := companyService.GetCompany(ctx, params.CompanyID) + if err != nil { + msg := fmt.Sprintf("User lookup for company by ID: %s failed : %v", params.CompanyID, err) + log.Warn(msg) + if _, ok := err.(*utils.CompanyNotFound); ok { + return signatures.NewListClaGroupCorporateContributorsBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ + Message: "EasyCLA - 404 Not Found - error getting company - " + msg, + Code: "404", + }) + } + return signatures.NewListClaGroupCorporateContributorsBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ + Message: "EasyCLA - 400 Bad Request - error getting company - " + msg, + Code: "400", + }) + } + + // Lookup the Project to CLA Group mapping table entries - this will have the correct details + projectCLAGroupEntries, projectCLAGroupErr := projectClaGroupsRepo.GetProjectsIdsForClaGroup(ctx, params.ClaGroupID) + // Should have at least one entry if we're set up correctly - it will have the foundation (parent project/project group) and project details set + if projectCLAGroupErr != nil || len(projectCLAGroupEntries) == 0 { + msg := fmt.Sprintf("unable to load project CLA Group mappings for CLA Group: %s - has this project been migrated to v2?", params.ClaGroupID) + log.WithFields(f).Warn(msg) + return signatures.NewListClaGroupCorporateContributorsBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequest(reqID, msg)) + } + // All the records will point to the same parent SFID + f["foundationSFID"] = projectCLAGroupEntries[0].FoundationSFID + log.WithFields(f).Debug("checking access control permissions for user...") - if !isUserHaveAccessToCLAProjectOrganization(ctx, authUser, claGroupModel.FoundationSFID, params.CompanySFID, projectClaGroupsRepo) { - msg := fmt.Sprintf(" user %s is not authorized to view project employee signatures any scope of project", - authUser.UserName) + if !isUserHaveAccessToCLAProjectOrganization(ctx, authUser, projectCLAGroupEntries[0].FoundationSFID, companyModel.CompanyExternalID, projectClaGroupsRepo) { + msg := fmt.Sprintf(" user %s is not authorized to view project employee signatures any scope of project", authUser.UserName) log.Warn(msg) return signatures.NewDownloadProjectSignatureEmployeeAsCSVForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } log.WithFields(f).Debug("user has access for this query") log.WithFields(f).Debug("searching for corporate contributor signatures...") - result, err := v2service.GetClaGroupCorporateContributorsCsv(ctx, params.ClaGroupID, params.CompanySFID) + result, err := v2SignatureService.GetClaGroupCorporateContributorsCsv(ctx, params.ClaGroupID, params.CompanyID) if err != nil { - msg := fmt.Sprintf("problem getting corporate contributors CSV for CLA Group: %s with company: %s", params.ClaGroupID, params.CompanySFID) + msg := fmt.Sprintf("problem getting corporate contributors CSV for CLA Group: %s with company: %s", params.ClaGroupID, companyModel.CompanyExternalID) if _, ok := err.(*organizations.GetOrgNotFound); ok { formatErr := errors.New("error retrieving company using companySFID") return signatures.NewDownloadProjectSignatureEmployeeAsCSVNotFound().WithXRequestID(reqID).WithPayload( @@ -769,20 +809,26 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje }) }) + // GET https://api-gw.platform.linuxfoundation.org/v4/cla-group/{claGroupID}/icla/signatures api.SignaturesListClaGroupIclaSignatureHandler = signatures.ListClaGroupIclaSignatureHandlerFunc(func(params signatures.ListClaGroupIclaSignatureParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "SignaturesListClaGroupIclaSignatureHandler", + "functionName": "v2.signatures.handlers.SignaturesListClaGroupIclaSignatureHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": params.ClaGroupID, + "searchTerm": utils.StringValue(params.SearchTerm), + "sortOrder": utils.StringValue(params.SortOrder), + "approved": utils.BoolValue(params.Approved), + "signed": utils.BoolValue(params.Signed), } log.WithFields(f).Debug("looking up CLA Group by ID...") - claGroupModel, err := projectService.GetCLAGroupByID(ctx, params.ClaGroupID) + claGroupModel, err := claGroupService.GetCLAGroupByID(ctx, params.ClaGroupID) if err != nil { log.WithFields(f).WithError(err).Warn(problemLoadingCLAGroupByID) - if err == project.ErrProjectDoesNotExist { + if err == repository.ErrProjectDoesNotExist { return signatures.NewListClaGroupIclaSignatureNotFound().WithXRequestID(reqID).WithPayload( utils.ErrorResponseNotFoundWithError(reqID, problemLoadingCLAGroupByID, err)) } @@ -808,10 +854,21 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje log.Warn(msg) return signatures.NewGetProjectCompanyEmployeeSignaturesForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } - log.WithFields(f).Debug("user has access for this query") log.WithFields(f).Debug("searching for ICLA signatures...") - result, err := v2service.GetProjectIclaSignatures(ctx, params.ClaGroupID, params.SearchTerm) + + var pageSize int64 + var nextKey string + + if params.PageSize != nil { + pageSize = *params.PageSize + } + + if params.NextKey != nil { + nextKey = *params.NextKey + } + + results, err := v2SignatureService.GetProjectIclaSignatures(ctx, params.ClaGroupID, params.SearchTerm, params.Approved, params.Signed, pageSize, nextKey, true) if err != nil { msg := fmt.Sprintf("problem loading ICLA signatures by CLA Group ID search term: %s", aws.StringValue(params.SearchTerm)) log.WithFields(f).WithError(err).Warn(msg) @@ -819,33 +876,26 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } - log.WithFields(f).Debugf("returning %d ICLA signatures to caller...", len(result.List)) - return signatures.NewListClaGroupIclaSignatureOK().WithXRequestID(reqID).WithPayload(result) + log.WithFields(f).Debugf("returning %d ICLA signatures to caller...", len(results.List)) + return signatures.NewListClaGroupIclaSignatureOK().WithXRequestID(reqID).WithPayload(results) }) api.SignaturesListClaGroupCorporateContributorsHandler = signatures.ListClaGroupCorporateContributorsHandlerFunc(func(params signatures.ListClaGroupCorporateContributorsParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "SignaturesListClaGroupCorporateContributorsHandler", + "functionName": "v2.signatures.handlers.SignaturesListClaGroupCorporateContributorsHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": params.ClaGroupID, - "companySFID": params.CompanySFID, - } - - // Make sure the user has provided the companySFID - if params.CompanySFID == nil { - msg := "missing companySFID as input" - log.WithFields(f).Warn(msg) - return signatures.NewListClaGroupCorporateContributorsBadRequest().WithXRequestID(reqID).WithPayload( - utils.ErrorResponseBadRequest(reqID, msg)) + "companyID": params.CompanyID, } // Lookup the CLA Group by ID - make sure it's valid - claGroupModel, err := projectRepo.GetCLAGroupByID(ctx, params.ClaGroupID, project.DontLoadRepoDetails) + claGroupModel, err := projectRepo.GetCLAGroupByID(ctx, params.ClaGroupID, repository.DontLoadRepoDetails) if err != nil { log.WithFields(f).WithError(err).Warn(problemLoadingCLAGroupByID) - if err == project.ErrProjectDoesNotExist { + if err == repository.ErrProjectDoesNotExist { return signatures.NewListClaGroupCorporateContributorsNotFound().WithXRequestID(reqID).WithPayload( utils.ErrorResponseNotFoundWithError(reqID, problemLoadingCLAGroupByID, err)) } @@ -854,32 +904,52 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje utils.ErrorResponseBadRequest(reqID, problemLoadingCLAGroupByID)) } + // Make sure the user has provided the companyID + if params.CompanyID == nil { + msg := "missing companyID as input" + log.WithFields(f).Warn(msg) + return signatures.NewListClaGroupCorporateContributorsBadRequest().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseBadRequest(reqID, msg)) + } + + companyModel, err := companyService.GetCompany(ctx, *params.CompanyID) + if err != nil { + msg := fmt.Sprintf("User lookup for company by ID: %s failed : %v", *params.CompanyID, err) + log.Warn(msg) + return signatures.NewListClaGroupCorporateContributorsBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + // Make sure CCLA is enabled for this CLA Group if !claGroupModel.ProjectCCLAEnabled { - msg := "cla group does not support corporate contribution" + msg := fmt.Sprintf("CLA Group with ID '%s' does not support corporate contribution", params.ClaGroupID) log.WithFields(f).Warn(msg) - // Return 200 as the retool UI can't handle 400's - return signatures.NewListClaGroupCorporateContributorsOK().WithXRequestID(reqID).WithPayload(&models.CorporateContributorList{ - List: []*models.CorporateContributor{}, // empty list - }) - //return signatures.NewListClaGroupCorporateContributorsBadRequest().WithXRequestID(reqID).WithPayload( - // utils.ErrorResponseBadRequest(reqID, msg)) + return signatures.NewListClaGroupCorporateContributorsBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, errors.New(msg))) } - f["foundationSFID"] = claGroupModel.FoundationSFID + + // Lookup the Project to CLA Group mapping table entries - this will have the correct details + projectCLAGroupEntries, projectCLAGroupErr := projectClaGroupsRepo.GetProjectsIdsForClaGroup(ctx, params.ClaGroupID) + // Should have at least one entry if we're set up correctly - it will have the foundation (parent project/project group) and project details set + if projectCLAGroupErr != nil || len(projectCLAGroupEntries) == 0 { + msg := fmt.Sprintf("unable to load project CLA Group mappings for CLA Group: %s - has this project been migrated to v2?", params.ClaGroupID) + log.WithFields(f).Warn(msg) + return signatures.NewListClaGroupCorporateContributorsBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequest(reqID, msg)) + } + // All the records will point to the same parent SFID + f["foundationSFID"] = projectCLAGroupEntries[0].FoundationSFID log.WithFields(f).Debug("checking access control permissions for user...") - if !isUserHaveAccessToCLAProjectOrganization(ctx, authUser, claGroupModel.FoundationSFID, *params.CompanySFID, projectClaGroupsRepo) { - msg := fmt.Sprintf("user %s is not authorized to view project CCLA signatures any scope of project or project|organization scope with company ID: %s", - authUser.UserName, aws.StringValue(params.CompanySFID)) + if !isUserHaveAccessToCLAProjectOrganization(ctx, authUser, projectCLAGroupEntries[0].FoundationSFID, companyModel.CompanyExternalID, projectClaGroupsRepo) { + msg := fmt.Sprintf("user '%s' is not authorized to view project CCLA signatures project scope or project|organization scope for company ID: %s", + authUser.UserName, companyModel.CompanyID) log.Warn(msg) return signatures.NewListClaGroupCorporateContributorsForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } log.WithFields(f).Debug("user has access for this query") - log.WithFields(f).Debug("searching for CCLA signatures...") - result, err := v2service.GetClaGroupCorporateContributors(ctx, params.ClaGroupID, params.CompanySFID, params.SearchTerm) + log.WithFields(f).Debug("searching for Coporate Contributors...") + result, err := v2SignatureService.GetClaGroupCorporateContributors(ctx, params) if err != nil { - msg := fmt.Sprintf("problem getting corporate contributors for CLA Group: %s with company: %s", params.ClaGroupID, *params.CompanySFID) + msg := fmt.Sprintf("problem getting corporate contributors for CLA Group: %s with company: %s", params.ClaGroupID, *params.CompanyID) if _, ok := err.(*organizations.GetOrgNotFound); ok { formatErr := errors.New("error retrieving company using companySFID") return signatures.NewListClaGroupCorporateContributorsNotFound().WithXRequestID(reqID).WithPayload( @@ -889,7 +959,7 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje utils.ErrorResponseInternalServerErrorWithError(reqID, "unexpected error when searching for corporate contributors", err)) } - log.WithFields(f).Debugf("returning %d CCLA signatures to caller...", len(result.List)) + log.WithFields(f).Debugf("returning %d Corporate contributors to caller...", len(result.List)) return signatures.NewListClaGroupCorporateContributorsOK().WithXRequestID(reqID).WithPayload(result) }) @@ -898,7 +968,7 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "SignaturesGetSignatureSignedDocumentHandler", + "functionName": "v2.signatures.handlers.SignaturesGetSignatureSignedDocumentHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "signatureID": params.SignatureID, } @@ -925,7 +995,7 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje utils.ErrorResponseForbidden(reqID, fmt.Sprintf("user %s does not have access to the specified signature", authUser.UserName))) } - doc, err := v2service.GetSignedDocument(ctx, signatureModel.SignatureID.String()) + doc, err := v2SignatureService.GetSignedDocument(ctx, signatureModel.SignatureID) if err != nil { log.WithFields(f).WithError(err).Warn("problem fetching signed document") if strings.Contains(err.Error(), "bad request") { @@ -941,17 +1011,18 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje api.SignaturesDownloadProjectSignatureICLAsHandler = signatures.DownloadProjectSignatureICLAsHandlerFunc(func(params signatures.DownloadProjectSignatureICLAsParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "SignaturesDownloadProjectSignatureICLAsHandler", + "functionName": "v2.signatures.handlers.SignaturesDownloadProjectSignatureICLAsHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": params.ClaGroupID, } log.WithFields(f).Debug("loading cla group by id...") - claGroupModel, err := projectService.GetCLAGroupByID(ctx, params.ClaGroupID) + claGroupModel, err := claGroupService.GetCLAGroupByID(ctx, params.ClaGroupID) if err != nil { log.WithFields(f).WithError(err).Warn(problemLoadingCLAGroupByID) - if err == project.ErrProjectDoesNotExist { + if err == repository.ErrProjectDoesNotExist { return signatures.NewDownloadProjectSignatureICLAsNotFound().WithXRequestID(reqID).WithPayload( utils.ErrorResponseNotFoundWithError(reqID, problemLoadingCLAGroupByID, err)) } @@ -975,7 +1046,7 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje log.WithFields(f).Debug("user has access for this query") log.WithFields(f).Debug("searching for ICLA signatures...") - result, err := v2service.GetSignedIclaZipPdf(params.ClaGroupID) + result, err := v2SignatureService.GetSignedIclaZipPdf(params.ClaGroupID) if err != nil { if err == ErrZipNotPresent { msg := "no icla signatures found for this cla group" @@ -995,17 +1066,18 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje api.SignaturesDownloadProjectSignatureICLAAsCSVHandler = signatures.DownloadProjectSignatureICLAAsCSVHandlerFunc(func(params signatures.DownloadProjectSignatureICLAAsCSVParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "SignaturesDownloadProjectSignatureICLAAsCSVHandler", + "functionName": "v2.signatures.handlers.SignaturesDownloadProjectSignatureICLAAsCSVHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": params.ClaGroupID, } log.WithFields(f).Debug("looking up CLA Group by ID...") - claGroupModel, err := projectService.GetCLAGroupByID(ctx, params.ClaGroupID) + claGroupModel, err := claGroupService.GetCLAGroupByID(ctx, params.ClaGroupID) if err != nil { log.WithFields(f).WithError(err).Warn(problemLoadingCLAGroupByID) - if err == project.ErrProjectDoesNotExist { + if err == repository.ErrProjectDoesNotExist { return signatures.NewDownloadProjectSignatureICLAAsCSVNotFound().WithXRequestID(reqID).WithPayload( utils.ErrorResponseNotFoundWithError(reqID, problemLoadingCLAGroupByID, err)) } @@ -1032,14 +1104,14 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje log.WithFields(f).Debug("checking access control permissions for user...") if !isUserHaveAccessToCLAGroupProjects(ctx, authUser, params.ClaGroupID, projectClaGroupsRepo, projectRepo) { - msg := fmt.Sprintf("user %s is not authorized to view project ICLA signatures any scope of project", authUser.UserName) + msg := fmt.Sprintf("user '%s' is not authorized to view project ICLA signatures any scope of project", authUser.UserName) log.Warn(msg) return signatures.NewDownloadProjectSignatureICLAAsCSVForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } log.WithFields(f).Debug("user has access for this query") log.WithFields(f).Debug("generating ICLA signatures for CSV...") - result, err := v2service.GetProjectIclaSignaturesCsv(ctx, params.ClaGroupID) + result, err := v2SignatureService.GetProjectIclaSignaturesCsv(ctx, params.ClaGroupID) if err != nil { msg := "unable to load ICLA signatures for CSV" log.WithFields(f).Warn(msg) @@ -1062,17 +1134,18 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje api.SignaturesDownloadProjectSignatureCCLAsHandler = signatures.DownloadProjectSignatureCCLAsHandlerFunc(func(params signatures.DownloadProjectSignatureCCLAsParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "SignaturesDownloadProjectSignatureCCLAsHandler", + "functionName": "v2.signatures.handlers.SignaturesDownloadProjectSignatureCCLAsHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": params.ClaGroupID, } log.WithFields(f).Debug("looking up CLA Group by ID...") - claGroupModel, err := projectService.GetCLAGroupByID(ctx, params.ClaGroupID) + claGroupModel, err := claGroupService.GetCLAGroupByID(ctx, params.ClaGroupID) if err != nil { log.WithFields(f).WithError(err).Warn(problemLoadingCLAGroupByID) - if err == project.ErrProjectDoesNotExist { + if err == repository.ErrProjectDoesNotExist { return signatures.NewDownloadProjectSignatureCCLAsNotFound().WithXRequestID(reqID).WithPayload( utils.ErrorResponseNotFoundWithError(reqID, problemLoadingCLAGroupByID, err)) } @@ -1095,7 +1168,7 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje log.WithFields(f).Debug("user has access for this query") log.WithFields(f).Debug("searching for CCLA signatures...") - result, err := v2service.GetSignedCclaZipPdf(params.ClaGroupID) + result, err := v2SignatureService.GetSignedCclaZipPdf(params.ClaGroupID) if err != nil { if err == ErrZipNotPresent { msg := "no ccla signatures found for this cla group" @@ -1115,17 +1188,18 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje api.SignaturesDownloadProjectSignatureCCLAAsCSVHandler = signatures.DownloadProjectSignatureCCLAAsCSVHandlerFunc(func(params signatures.DownloadProjectSignatureCCLAAsCSVParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "SignaturesDownloadProjectSignatureCCLAAsCSVHandler", + "functionName": "v2.signatures.handlers.SignaturesDownloadProjectSignatureCCLAAsCSVHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": params.ClaGroupID, } log.WithFields(f).Debug("looking up CLA Group by ID...") - claGroupModel, err := projectService.GetCLAGroupByID(ctx, params.ClaGroupID) + claGroupModel, err := claGroupService.GetCLAGroupByID(ctx, params.ClaGroupID) if err != nil { log.WithFields(f).WithError(err).Warn(problemLoadingCLAGroupByID) - if err == project.ErrProjectDoesNotExist { + if err == repository.ErrProjectDoesNotExist { return signatures.NewDownloadProjectSignatureCCLAAsCSVNotFound().WithXRequestID(reqID).WithPayload( utils.ErrorResponseNotFoundWithError(reqID, problemLoadingCLAGroupByID, err)) } @@ -1152,14 +1226,14 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje log.WithFields(f).Debug("checking access control permissions for user...") if !isUserHaveAccessToCLAGroupProjects(ctx, authUser, params.ClaGroupID, projectClaGroupsRepo, projectRepo) { - msg := fmt.Sprintf("user %s is not authorized to view project CCLA signatures any scope of project", authUser.UserName) + msg := fmt.Sprintf("user '%s' is not authorized to view project CCLA signatures any scope of project", authUser.UserName) log.Warn(msg) return signatures.NewDownloadProjectSignatureCCLAAsCSVForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } log.WithFields(f).Debug("user has access for this query") log.WithFields(f).Debug("generating ICLA signatures for CSV...") - result, err := v2service.GetProjectCclaSignaturesCsv(ctx, params.ClaGroupID) + result, err := v2SignatureService.GetProjectCclaSignaturesCsv(ctx, params.ClaGroupID) if err != nil { msg := "unable to load CCLA signatures for CSV" log.WithFields(f).Warn(msg) @@ -1178,6 +1252,170 @@ func Configure(api *operations.EasyclaAPI, projectService project.Service, proje } }) }) + api.SignaturesInvalidateICLAHandler = signatures.InvalidateICLAHandlerFunc(func(params signatures.InvalidateICLAParams, authUser *auth.User) middleware.Responder { + reqID := utils.GetRequestID(params.XREQUESTID) + ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + f := logrus.Fields{ + "functionName": "v2.signatures.handlers.SignaturesInvalidateICLAHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": params.ClaGroupID, + "userID": params.UserID, + } + log.WithFields(f).Debug("Invalidating ICLA record...") + eventArgs := &events.LogEventArgs{ + EventType: events.InvalidatedSignature, + EventData: &events.SignatureProjectInvalidatedEventData{ + InvalidatedCount: 1, + }, + } + err := v2SignatureService.InvalidateICLA(ctx, params.ClaGroupID, params.UserID, authUser, eventsService, eventArgs) + if err != nil { + msg := "unable to invalidate icla" + log.WithFields(f).Warn(msg) + // return signatures.NewInvalidateSignatureBadRequest().WithXRequestID(reqID).WithPayload( + // utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + return signatures.NewInvalidateICLABadRequest().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + return signatures.NewInvalidateICLAOK().WithXRequestID(reqID) + }) + + api.SignaturesEclaAutoCreateHandler = signatures.EclaAutoCreateHandlerFunc(func(eacp signatures.EclaAutoCreateParams, u *auth.User) middleware.Responder { + reqID := utils.GetRequestID(eacp.XREQUESTID) + ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + utils.SetAuthUserProperties(u, eacp.XUSERNAME, eacp.XEMAIL) + f := logrus.Fields{ + "functionName": "v2.signatures.handlers.SignaturesEclaAutoCreateHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": eacp.ClaGroupID, + "companyID": eacp.CompanyID, + "autoCreateEclaFlag": eacp.Body.AutoCreateEcla, + } + + if eacp.Body == nil { + return signatures.NewEclaAutoCreateBadRequest().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseBadRequest(reqID, "missing request body")) + } else { + f["autoCreateEclaFlag"] = eacp.Body.AutoCreateEcla + } + + log.WithFields(f).Debug("Updating CCLA signature for the auto_create_ecla column...") + + log.WithFields(f).Debug("Loading the corporate signature...") + approved := true + signed := true + + cclaSignature, err := v1SignatureService.GetCorporateSignature(ctx, eacp.ClaGroupID, eacp.CompanyID, &approved, &signed) + if err != nil { + msg := "unable to load corporate signature" + log.WithFields(f).Warn(msg) + return signatures.NewEclaAutoCreateBadRequest().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + + companyRecord, err := companyService.GetCompany(ctx, eacp.CompanyID) + if err != nil { + msg := "unable to load company" + log.WithFields(f).Warn(msg) + return signatures.NewEclaAutoCreateBadRequest().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + + claGroup, err := claGroupService.GetCLAGroupByID(ctx, eacp.ClaGroupID) + if err != nil { + msg := "unable to load CLA Group" + log.WithFields(f).Warn(msg) + return signatures.NewEclaAutoCreateBadRequest().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + + // Ensure current user is in the Signature ACL + claManagers := cclaSignature.SignatureACL + if !utils.CurrentUserInACL(u, claManagers) { + msg := fmt.Sprintf("EasyCLA - 403 Forbidden - CLA Manager %s / %s is not authorized to approve request for company ID: %s / %s / %s, project ID: %s / %s / %s", + u.UserName, u.Email, + cclaSignature.CompanyName, companyRecord.CompanyExternalID, companyRecord.CompanyID, + claGroup.ProjectName, claGroup.ProjectExternalID, cclaSignature.ProjectID) + return signatures.NewEclaAutoCreateForbidden().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseForbidden(reqID, msg)) + } + + err = v2SignatureService.EclaAutoCreate(ctx, cclaSignature.SignatureID, eacp.Body.AutoCreateEcla) + if err != nil { + msg := "unable to update auto_create_ecla flag" + log.WithFields(f).Warn(msg) + return signatures.NewEclaAutoCreateBadRequest().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + eventsService.LogEvent(&events.LogEventArgs{ + EventType: events.SignatureAutoCreateECLAUpdated, + CLAGroupID: eacp.ClaGroupID, + CompanyID: eacp.CompanyID, + LfUsername: u.UserName, + CLAGroupName: claGroup.ProjectName, + CompanyName: companyRecord.CompanyName, + EventData: &events.SignatureAutoCreateECLAUpdatedEventData{ + AutoCreateECLA: eacp.Body.AutoCreateEcla, + }, + }) + }() + + // Only work to create ECLA records when the auto-enable flag is set to true + if eacp.Body.AutoCreateEcla { + wg.Add(1) + + go func() { + defer wg.Done() + // Reload the CCLA signature record + cclaSignatureRecord, readErr := v1SignatureService.GetSignature(ctx, cclaSignature.SignatureID) + if readErr != nil { + msg := fmt.Sprintf("problem loading existing CCLA signature record for signature ID: %s", cclaSignature.SignatureID) + log.WithFields(f).WithError(readErr).Warn(msg) + } + + _, processErr := v1SignatureService.CreateOrUpdateEmployeeSignature(ctx, claGroup, companyRecord, cclaSignatureRecord) + if processErr != nil { + msg := fmt.Sprintf("problem processing auto-enable request for company ID: %s, project ID: %s, cla group ID: %s", companyRecord.CompanyID, claGroup.ProjectID, eacp.ClaGroupID) + log.WithFields(f).WithError(processErr).Warn(msg) + } + }() + } + + // Wait until all the workers are done + wg.Wait() + + return signatures.NewEclaAutoCreateOK().WithXRequestID(reqID) + }) + + api.SignaturesIsAuthorizedHandler = signatures.IsAuthorizedHandlerFunc(func(params signatures.IsAuthorizedParams) middleware.Responder { + reqID := utils.GetRequestID(params.XREQUESTID) + ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + f := logrus.Fields{ + "functionName": "v2.signatures.handlers.SignaturesIsAuthorizedHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "lfid": params.Lfid, + "clagroupid": params.ClaGroupID, + } + + log.WithFields(f).Debug("checking if user is authorized...") + result, err := v2SignatureService.IsUserAuthorized(ctx, params.Lfid, params.ClaGroupID) + if err != nil { + msg := "problem checking if user is authorized" + log.WithFields(f).WithError(err).Warn(msg) + return signatures.NewIsAuthorizedBadRequest().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + + log.WithFields(f).Debug("returning authorization result to caller...") + return signatures.NewIsAuthorizedOK().WithXRequestID(reqID).WithPayload(result) + }) } // getProjectIDsFromModels is a helper function to extract the project SFIDs from the project CLA Group models @@ -1194,9 +1432,9 @@ func getProjectIDsFromModels(f logrus.Fields, foundationSFID string, projectCLAG } // isUserHaveAccessOfSignedSignaturePDF returns true if the specified user has access to the provided signature, false otherwise -func isUserHaveAccessOfSignedSignaturePDF(ctx context.Context, authUser *auth.User, signature *v1Models.Signature, companyService company.IService, projectClaGroupRepo projects_cla_groups.Repository, projectRepo project.ProjectRepository) (bool, error) { +func isUserHaveAccessOfSignedSignaturePDF(ctx context.Context, authUser *auth.User, signature *v1Models.Signature, companyService company.IService, projectClaGroupRepo projects_cla_groups.Repository, projectRepo repository.ProjectRepository) (bool, error) { f := logrus.Fields{ - "functionName": "isUserHaveAccessOfSignedSignaturePDF", + "functionName": "v2.signatures.handlers.isUserHaveAccessOfSignedSignaturePDF", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "authUserName": authUser.UserName, "authUserEmail": authUser.Email, @@ -1207,7 +1445,7 @@ func isUserHaveAccessOfSignedSignaturePDF(ctx context.Context, authUser *auth.Us } var projectCLAGroup *v1Models.ClaGroup - projects, err := projectClaGroupRepo.GetProjectsIdsForClaGroup(signature.ProjectID) + projects, err := projectClaGroupRepo.GetProjectsIdsForClaGroup(ctx, signature.ProjectID) if err != nil { log.WithFields(f).WithError(err).Warn("error loading load project IDs for CLA Group") return false, err @@ -1240,41 +1478,41 @@ func isUserHaveAccessOfSignedSignaturePDF(ctx context.Context, authUser *auth.Us f["foundationSFID"] = foundationID // First, check for PM access - if utils.IsUserAuthorizedForProjectTree(authUser, foundationID, utils.ALLOW_ADMIN_SCOPE) { + if utils.IsUserAuthorizedForProjectTree(ctx, authUser, foundationID, utils.ALLOW_ADMIN_SCOPE) { log.WithFields(f).Debugf("user is authorized for %s scope for foundation ID: %s", utils.ProjectScope, foundationID) return true, nil } // In case the project tree didn't pass, let's check the project list individually - if any has access, we return true for _, proj := range projects { - if utils.IsUserAuthorizedForProject(authUser, proj.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + if utils.IsUserAuthorizedForProject(ctx, authUser, proj.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { log.WithFields(f).Debugf("user is authorized for %s scope for project ID: %s", utils.ProjectScope, proj.ProjectSFID) return true, nil } } // Corporate signature...we can check the company details - if signature.SignatureType == CclaSignatureType { - comp, err := companyService.GetCompany(ctx, signature.SignatureReferenceID.String()) + if signature.SignatureType == utils.SignatureTypeCCLA { + comp, err := companyService.GetCompany(ctx, signature.SignatureReferenceID) if err != nil { - log.WithFields(f).WithError(err).Warnf("failed to load company record using signature reference id: %s", signature.SignatureReferenceID.String()) + log.WithFields(f).WithError(err).Warnf("failed to load company record using signature reference id: %s", signature.SignatureReferenceID) return false, err } // No company SFID? Then, we can't check permissions... if comp == nil || comp.CompanyExternalID == "" { - log.WithFields(f).Warnf("failed to load company record with external SFID using signature reference id: %s", signature.SignatureReferenceID.String()) + log.WithFields(f).Warnf("failed to load company record with external SFID using signature reference id: %s", signature.SignatureReferenceID) return false, err } // Check the project|org tree starting with the foundation - if utils.IsUserAuthorizedForProjectOrganizationTree(authUser, foundationID, comp.CompanyExternalID, utils.ALLOW_ADMIN_SCOPE) { + if utils.IsUserAuthorizedForProjectOrganizationTree(ctx, authUser, foundationID, comp.CompanyExternalID, utils.ALLOW_ADMIN_SCOPE) { return true, nil } // In case the project organization tree didn't pass, let's check the project list individually - if any has access, we return true for _, proj := range projects { - if utils.IsUserAuthorizedForProjectOrganization(authUser, proj.ProjectSFID, comp.CompanyExternalID, utils.ALLOW_ADMIN_SCOPE) { + if utils.IsUserAuthorizedForProjectOrganization(ctx, authUser, proj.ProjectSFID, comp.CompanyExternalID, utils.ALLOW_ADMIN_SCOPE) { log.WithFields(f).Debugf("user is authorized for %s scope for project ID: %s, org iD: %s", utils.ProjectOrgScope, proj.ProjectSFID, comp.CompanyExternalID) return true, nil } @@ -1305,9 +1543,9 @@ func errorResponse(reqID string, err error) *models.ErrorResponse { } // isUserHaveAccessToCLAGroupProjects is a helper function to determine if the user has access to the specified CLA Group projects -func isUserHaveAccessToCLAGroupProjects(ctx context.Context, authUser *auth.User, claGroupID string, projectClaGroupsRepo projects_cla_groups.Repository, projectRepo project.ProjectRepository) bool { +func isUserHaveAccessToCLAGroupProjects(ctx context.Context, authUser *auth.User, claGroupID string, projectClaGroupsRepo projects_cla_groups.Repository, projectRepo repository.ProjectRepository) bool { f := logrus.Fields{ - "functionName": "isUserHaveAccessToCLAGroupProjects", + "functionName": "v2.signatures.handlers.isUserHaveAccessToCLAGroupProjects", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": claGroupID, "userName": authUser.UserName, @@ -1318,7 +1556,7 @@ func isUserHaveAccessToCLAGroupProjects(ctx context.Context, authUser *auth.User // Lookup the project IDs for the CLA Group log.WithFields(f).Debug("looking up projects associated with the CLA Group...") - projectCLAGroupModels, err := projectClaGroupsRepo.GetProjectsIdsForClaGroup(claGroupID) + projectCLAGroupModels, err := projectClaGroupsRepo.GetProjectsIdsForClaGroup(ctx, claGroupID) if err != nil { log.WithFields(f).WithError(err).Warnf("problem loading project cla group mappings by CLA Group ID - failed permission check") return false @@ -1349,11 +1587,11 @@ func isUserHaveAccessToCLAGroupProjects(ctx context.Context, authUser *auth.User foundationSFID := projectCLAGroupModels[0].FoundationSFID f["foundationSFID"] = foundationSFID log.WithFields(f).Debug("testing if user has access to parent foundation...") - if utils.IsUserAuthorizedForProjectTree(authUser, foundationSFID, utils.ALLOW_ADMIN_SCOPE) { + if utils.IsUserAuthorizedForProjectTree(ctx, authUser, foundationSFID, utils.ALLOW_ADMIN_SCOPE) { log.WithFields(f).Debug("user has access to parent foundation tree...") return true } - if utils.IsUserAuthorizedForProject(authUser, foundationSFID, utils.ALLOW_ADMIN_SCOPE) { + if utils.IsUserAuthorizedForProject(ctx, authUser, foundationSFID, utils.ALLOW_ADMIN_SCOPE) { log.WithFields(f).Debug("user has access to parent foundation...") return true } @@ -1362,65 +1600,7 @@ func isUserHaveAccessToCLAGroupProjects(ctx context.Context, authUser *auth.User projectSFIDs := getProjectIDsFromModels(f, foundationSFID, projectCLAGroupModels) f["projectIDs"] = strings.Join(projectSFIDs, ",") log.WithFields(f).Debug("testing if user has access to any projects") - if utils.IsUserAuthorizedForAnyProjects(authUser, projectSFIDs, utils.ALLOW_ADMIN_SCOPE) { - log.WithFields(f).Debug("user has access to at least of of the projects...") - return true - } - - log.WithFields(f).Debug("exhausted project checks - user does not have access to project") - return false -} - -// isUserHaveAccessToCLAProject is a helper function to determine if the user has access to the specified project -func isUserHaveAccessToCLAProject(ctx context.Context, authUser *auth.User, projectSFID string, projectClaGroupsRepo projects_cla_groups.Repository) bool { // nolint - f := logrus.Fields{ - "functionName": "isUserHaveAccessToCLAProject", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "projectSFID": projectSFID, - "userName": authUser.UserName, - "userEmail": authUser.Email, - } - - log.WithFields(f).Debug("testing if user has access to project SFID") - if utils.IsUserAuthorizedForProject(authUser, projectSFID, utils.ALLOW_ADMIN_SCOPE) { - return true - } - - log.WithFields(f).Debug("user doesn't have direct access to the projectSFID - loading CLA Group from project id...") - projectCLAGroupModel, err := projectClaGroupsRepo.GetClaGroupIDForProject(projectSFID) - if err != nil { - log.WithFields(f).WithError(err).Warnf("problem loading project -> cla group mapping - returning false") - return false - } - if projectCLAGroupModel == nil { - log.WithFields(f).WithError(err).Warnf("problem loading project -> cla group mapping - no mapping found - returning false") - return false - } - - f["foundationSFID"] = projectCLAGroupModel.FoundationSFID - log.WithFields(f).Debug("testing if user has access to parent foundation...") - if utils.IsUserAuthorizedForProjectTree(authUser, projectCLAGroupModel.FoundationSFID, utils.ALLOW_ADMIN_SCOPE) { - log.WithFields(f).Debug("user has access to parent foundation tree...") - return true - } - if utils.IsUserAuthorizedForProject(authUser, projectCLAGroupModel.FoundationSFID, utils.ALLOW_ADMIN_SCOPE) { - log.WithFields(f).Debug("user has access to parent foundation...") - return true - } - log.WithFields(f).Debug("user does not have access to parent foundation...") - - // Lookup the other project IDs for the CLA Group - log.WithFields(f).Debug("looking up other projects associated with the CLA Group...") - projectCLAGroupModels, err := projectClaGroupsRepo.GetProjectsIdsForClaGroup(projectCLAGroupModel.ClaGroupID) - if err != nil { - log.WithFields(f).WithError(err).Warnf("problem loading project cla group mappings by CLA Group ID - returning false") - return false - } - - projectSFIDs := getProjectIDsFromModels(f, projectCLAGroupModel.FoundationSFID, projectCLAGroupModels) - f["projectIDs"] = strings.Join(projectSFIDs, ",") - log.WithFields(f).Debug("testing if user has access to any projects") - if utils.IsUserAuthorizedForAnyProjects(authUser, projectSFIDs, utils.ALLOW_ADMIN_SCOPE) { + if utils.IsUserAuthorizedForAnyProjects(ctx, authUser, projectSFIDs, utils.ALLOW_ADMIN_SCOPE) { log.WithFields(f).Debug("user has access to at least of of the projects...") return true } @@ -1432,7 +1612,7 @@ func isUserHaveAccessToCLAProject(ctx context.Context, authUser *auth.User, proj // isUserHaveAccessToCLAProjectOrganization is a helper function to determine if the user has access to the specified project and organization func isUserHaveAccessToCLAProjectOrganization(ctx context.Context, authUser *auth.User, projectSFID, organizationSFID string, projectClaGroupsRepo projects_cla_groups.Repository) bool { f := logrus.Fields{ - "functionName": "isUserHaveAccessToCLAProjectOrganization", + "functionName": "v2.signatures.handlers.isUserHaveAccessToCLAProjectOrganization", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": projectSFID, "organizationSFID": organizationSFID, @@ -1440,41 +1620,41 @@ func isUserHaveAccessToCLAProjectOrganization(ctx context.Context, authUser *aut "userEmail": authUser.Email, } - log.WithFields(f).Debug("testing if user has access to project SFID...") - if utils.IsUserAuthorizedForProject(authUser, projectSFID, utils.ALLOW_ADMIN_SCOPE) { - log.WithFields(f).Debug("user has access to project SFID...") + log.WithFields(f).Debugf("testing if user %s/%s has access to project SFID: %s...", authUser.UserName, authUser.Email, projectSFID) + if utils.IsUserAuthorizedForProject(ctx, authUser, projectSFID, utils.ALLOW_ADMIN_SCOPE) { + log.WithFields(f).Debugf("user %s/%s has access to project SFID: %s...", authUser.UserName, authUser.Email, projectSFID) return true } - log.WithFields(f).Debug("testing if user has access to project SFID tree...") - if utils.IsUserAuthorizedForProjectTree(authUser, projectSFID, utils.ALLOW_ADMIN_SCOPE) { - log.WithFields(f).Debug("user has access to project SFID tree...") + log.WithFields(f).Debugf("testing if user %s/%s has access to project SFID tree...", authUser.UserName, authUser.Email) + if utils.IsUserAuthorizedForProjectTree(ctx, authUser, projectSFID, utils.ALLOW_ADMIN_SCOPE) { + log.WithFields(f).Debugf("user %s/%s has access to project SFID tree...", authUser.UserName, authUser.Email) return true } - log.WithFields(f).Debug("testing if user has access to project SFID and organization SFID...") - if utils.IsUserAuthorizedForProjectOrganization(authUser, projectSFID, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { - log.WithFields(f).Debug("user has access to project SFID and organization SFID...") + log.WithFields(f).Debugf("testing if user %s/%s has access to project SFID and organization SFID...", authUser.UserName, authUser.Email) + if utils.IsUserAuthorizedForProjectOrganization(ctx, authUser, projectSFID, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { + log.WithFields(f).Debugf("user %s/%s has access to project SFID and organization SFID...", authUser.UserName, authUser.Email) return true } - log.WithFields(f).Debug("testing if user has access to project SFID and organization SFID tree...") - if utils.IsUserAuthorizedForProjectOrganizationTree(authUser, projectSFID, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { - log.WithFields(f).Debug("user has access to project SFID and organization SFID tree...") + log.WithFields(f).Debugf("testing if user %s/%s has access to project SFID and organization SFID tree...", authUser.UserName, authUser.Email) + if utils.IsUserAuthorizedForProjectOrganizationTree(ctx, authUser, projectSFID, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { + log.WithFields(f).Debugf("user %s/%s has access to project SFID and organization SFID tree...", authUser.UserName, authUser.Email) return true } - log.WithFields(f).Debug("testing if user has access to organization SFID...") - if utils.IsUserAuthorizedForOrganization(authUser, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { - log.WithFields(f).Debug("user has access to organization SFID...") + log.WithFields(f).Debugf("testing if user %s/%s has access to organization SFID...", authUser.UserName, authUser.Email) + if utils.IsUserAuthorizedForOrganization(ctx, authUser, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { + log.WithFields(f).Debugf("user %s/%s has access to organization SFID...", authUser.UserName, authUser.Email) return true } // No luck so far...let's load up the Project => CLA Group mapping and check to see if the user has access to the // other projects or the parent project group/foundation - log.WithFields(f).Debug("user doesn't have direct access to the project only, project + organization, or organization only - loading CLA Group from project id...") - projectCLAGroupModel, err := projectClaGroupsRepo.GetClaGroupIDForProject(projectSFID) + log.WithFields(f).Debugf("user %s/%s doesn't have direct access to the project only, project + organization, or organization only - loading CLA Group from project id...", authUser.UserName, authUser.Email) + projectCLAGroupModel, err := projectClaGroupsRepo.GetClaGroupIDForProject(ctx, projectSFID) if err != nil { log.WithFields(f).WithError(err).Warnf("problem loading project -> cla group mapping - returning false") return false @@ -1486,45 +1666,55 @@ func isUserHaveAccessToCLAProjectOrganization(ctx context.Context, authUser *aut // Check the foundation permissions f["foundationSFID"] = projectCLAGroupModel.FoundationSFID - log.WithFields(f).Debug("testing if user has access to parent foundation...") - if utils.IsUserAuthorizedForProject(authUser, projectCLAGroupModel.FoundationSFID, utils.ALLOW_ADMIN_SCOPE) { - log.WithFields(f).Debug("user has access to parent foundation...") + log.WithFields(f).Debugf("testing if user %s/%s has access to parent foundation SFID: %s...", authUser.UserName, authUser.Email, projectCLAGroupModel.FoundationSFID) + if utils.IsUserAuthorizedForProject(ctx, authUser, projectCLAGroupModel.FoundationSFID, utils.ALLOW_ADMIN_SCOPE) { + log.WithFields(f).Debugf("user %s/%s has access to parent foundation SFID: %s...", authUser.UserName, authUser.Email, projectCLAGroupModel.FoundationSFID) return true } - log.WithFields(f).Debug("testing if user has access to parent foundation tree...") - if utils.IsUserAuthorizedForProjectTree(authUser, projectCLAGroupModel.FoundationSFID, utils.ALLOW_ADMIN_SCOPE) { - log.WithFields(f).Debug("user has access to parent foundation tree...") + + log.WithFields(f).Debugf("testing if user %s/%s has access to parent foundation SFID: %s tree...", authUser.UserName, authUser.Email, projectCLAGroupModel.FoundationSFID) + if utils.IsUserAuthorizedForProjectTree(ctx, authUser, projectCLAGroupModel.FoundationSFID, utils.ALLOW_ADMIN_SCOPE) { + log.WithFields(f).Debugf("user %s/%s has access to parent foundation SFID: %s tree...", authUser.UserName, authUser.Email, projectCLAGroupModel.FoundationSFID) return true } - log.WithFields(f).Debug("testing if user has access to foundation SFID and organization SFID...") - if utils.IsUserAuthorizedForProjectOrganization(authUser, projectCLAGroupModel.FoundationSFID, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { - log.WithFields(f).Debug("user has access to foundation SFID and organization SFID...") + log.WithFields(f).Debugf("testing if user %s/%s has access to foundation SFID %s and organization SFID %s ...", authUser.UserName, authUser.Email, projectCLAGroupModel.FoundationSFID, organizationSFID) + if utils.IsUserAuthorizedForProjectOrganization(ctx, authUser, projectCLAGroupModel.FoundationSFID, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { + log.WithFields(f).Debugf("user %s/%s has access to foundation SFID %s and organization SFID %s...", authUser.UserName, authUser.Email, projectCLAGroupModel.FoundationSFID, organizationSFID) return true } - log.WithFields(f).Debug("testing if user has access to foundation SFID and organization SFID tree...") - if utils.IsUserAuthorizedForProjectOrganizationTree(authUser, projectCLAGroupModel.FoundationSFID, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { - log.WithFields(f).Debug("user has access to foundation SFID and organization SFID tree...") + log.WithFields(f).Debugf("testing if user %s/%s has access to foundation SFID %s and organization SFID %s tree...", authUser.UserName, authUser.Email, projectCLAGroupModel.FoundationSFID, organizationSFID) + if utils.IsUserAuthorizedForProjectOrganizationTree(ctx, authUser, projectCLAGroupModel.FoundationSFID, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { + log.WithFields(f).Debugf("user %s/%s has access to foundation SFID %s and organization SFID %s tree...", authUser.UserName, authUser.Email, projectCLAGroupModel.FoundationSFID, organizationSFID) return true } // Lookup the other project IDs associated with this CLA Group log.WithFields(f).Debug("looking up other projects associated with the CLA Group...") - projectCLAGroupModels, err := projectClaGroupsRepo.GetProjectsIdsForClaGroup(projectCLAGroupModel.ClaGroupID) + projectCLAGroupModels, err := projectClaGroupsRepo.GetProjectsIdsForClaGroup(ctx, projectCLAGroupModel.ClaGroupID) if err != nil { log.WithFields(f).WithError(err).Warnf("problem loading project cla group mappings by CLA Group ID - returning false") return false } + // Get the list of the project group and projects associated with this CLA Group projectSFIDs := getProjectIDsFromModels(f, projectCLAGroupModel.FoundationSFID, projectCLAGroupModels) - f["projectIDs"] = strings.Join(projectSFIDs, ",") - log.WithFields(f).Debug("testing if user has access to any cla group project + organization") - if utils.IsUserAuthorizedForAnyProjectOrganization(authUser, projectSFIDs, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { - log.WithFields(f).Debug("user has access to at least of of the projects...") + projectSFIDsCSV := strings.Join(projectSFIDs, ",") // Create a project SFID CSV for printout + f["projectIDs"] = projectSFIDsCSV + + log.WithFields(f).Debugf("testing if user %s/%s has access to any cla group projects: %s", authUser.UserName, authUser.Email, projectSFIDsCSV) + if utils.IsUserAuthorizedForAnyProjects(ctx, authUser, projectSFIDs, utils.ALLOW_ADMIN_SCOPE) { + log.WithFields(f).Debugf("user %s/%s has access to at least of of the projects: %s...", authUser.UserName, authUser.Email, projectSFIDsCSV) return true } - log.WithFields(f).Debug("exhausted project checks - user does not have access to project") + log.WithFields(f).Debugf("testing if user %s/%s has access to any cla group projects: %s + organization SFID: %s", authUser.UserName, authUser.Email, projectSFIDsCSV, organizationSFID) + if utils.IsUserAuthorizedForAnyProjectOrganization(ctx, authUser, projectSFIDs, organizationSFID, utils.ALLOW_ADMIN_SCOPE) { + log.WithFields(f).Debugf("user %s/%s has access to at least of of the projects: %s + organization SFID: %s...", authUser.UserName, authUser.Email, projectSFIDsCSV, organizationSFID) + return true + } + + log.WithFields(f).Debugf("exhausted project checks - user %s/%s does not have access to project", authUser.UserName, authUser.Email) return false } diff --git a/cla-backend-go/v2/signatures/mock_users/mock_service.go b/cla-backend-go/v2/signatures/mock_users/mock_service.go new file mode 100644 index 000000000..822185c25 --- /dev/null +++ b/cla-backend-go/v2/signatures/mock_users/mock_service.go @@ -0,0 +1,247 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +// Code generated by MockGen. DO NOT EDIT. +// Source: users/service.go + +// Package mock_users is a generated GoMock package. +package mock_users + +import ( + reflect "reflect" + + models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + user "github.com/communitybridge/easycla/cla-backend-go/user" + gomock "github.com/golang/mock/gomock" +) + +// MockService is a mock of Service interface. +type MockService struct { + ctrl *gomock.Controller + recorder *MockServiceMockRecorder +} + +// MockServiceMockRecorder is the mock recorder for MockService. +type MockServiceMockRecorder struct { + mock *MockService +} + +// NewMockService creates a new mock instance. +func NewMockService(ctrl *gomock.Controller) *MockService { + mock := &MockService{ctrl: ctrl} + mock.recorder = &MockServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockService) EXPECT() *MockServiceMockRecorder { + return m.recorder +} + +// CreateUser mocks base method. +func (m *MockService) CreateUser(user *models.User, claUser *user.CLAUser) (*models.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateUser", user, claUser) + ret0, _ := ret[0].(*models.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateUser indicates an expected call of CreateUser. +func (mr *MockServiceMockRecorder) CreateUser(user, claUser interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockService)(nil).CreateUser), user, claUser) +} + +// Delete mocks base method. +func (m *MockService) Delete(userID string, claUser *user.CLAUser) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", userID, claUser) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockServiceMockRecorder) Delete(userID, claUser interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockService)(nil).Delete), userID, claUser) +} + +// GetUser mocks base method. +func (m *MockService) GetUser(userID string) (*models.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUser", userID) + ret0, _ := ret[0].(*models.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUser indicates an expected call of GetUser. +func (mr *MockServiceMockRecorder) GetUser(userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockService)(nil).GetUser), userID) +} + +// GetUserByEmail mocks base method. +func (m *MockService) GetUserByEmail(userEmail string) (*models.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserByEmail", userEmail) + ret0, _ := ret[0].(*models.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserByEmail indicates an expected call of GetUserByEmail. +func (mr *MockServiceMockRecorder) GetUserByEmail(userEmail interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByEmail", reflect.TypeOf((*MockService)(nil).GetUserByEmail), userEmail) +} + +// GetUserByGitHubID mocks base method. +func (m *MockService) GetUserByGitHubID(gitHubID string) (*models.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserByGitHubID", gitHubID) + ret0, _ := ret[0].(*models.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserByGitHubID indicates an expected call of GetUserByGitHubID. +func (mr *MockServiceMockRecorder) GetUserByGitHubID(gitHubID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByGitHubID", reflect.TypeOf((*MockService)(nil).GetUserByGitHubID), gitHubID) +} + +// GetUserByGitHubUsername mocks base method. +func (m *MockService) GetUserByGitHubUsername(gitlabUsername string) (*models.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserByGitHubUsername", gitlabUsername) + ret0, _ := ret[0].(*models.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserByGitHubUsername indicates an expected call of GetUserByGitHubUsername. +func (mr *MockServiceMockRecorder) GetUserByGitHubUsername(gitlabUsername interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByGitHubUsername", reflect.TypeOf((*MockService)(nil).GetUserByGitHubUsername), gitlabUsername) +} + +// GetUserByGitLabUsername mocks base method. +func (m *MockService) GetUserByGitLabUsername(gitlabUsername string) (*models.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserByGitLabUsername", gitlabUsername) + ret0, _ := ret[0].(*models.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserByGitLabUsername indicates an expected call of GetUserByGitLabUsername. +func (mr *MockServiceMockRecorder) GetUserByGitLabUsername(gitlabUsername interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByGitLabUsername", reflect.TypeOf((*MockService)(nil).GetUserByGitLabUsername), gitlabUsername) +} + +// GetUserByGitlabID mocks base method. +func (m *MockService) GetUserByGitlabID(gitHubID int) (*models.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserByGitlabID", gitHubID) + ret0, _ := ret[0].(*models.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserByGitlabID indicates an expected call of GetUserByGitlabID. +func (mr *MockServiceMockRecorder) GetUserByGitlabID(gitHubID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByGitlabID", reflect.TypeOf((*MockService)(nil).GetUserByGitlabID), gitHubID) +} + +// GetUserByLFUserName mocks base method. +func (m *MockService) GetUserByLFUserName(lfUserName string) (*models.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserByLFUserName", lfUserName) + ret0, _ := ret[0].(*models.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserByLFUserName indicates an expected call of GetUserByLFUserName. +func (mr *MockServiceMockRecorder) GetUserByLFUserName(lfUserName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByLFUserName", reflect.TypeOf((*MockService)(nil).GetUserByLFUserName), lfUserName) +} + +// GetUserByUserName mocks base method. +func (m *MockService) GetUserByUserName(userName string, fullMatch bool) (*models.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserByUserName", userName, fullMatch) + ret0, _ := ret[0].(*models.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserByUserName indicates an expected call of GetUserByUserName. +func (mr *MockServiceMockRecorder) GetUserByUserName(userName, fullMatch interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUserName", reflect.TypeOf((*MockService)(nil).GetUserByUserName), userName, fullMatch) +} + +// Save mocks base method. +func (m *MockService) Save(user *models.UserUpdate, claUser *user.CLAUser) (*models.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Save", user, claUser) + ret0, _ := ret[0].(*models.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Save indicates an expected call of Save. +func (mr *MockServiceMockRecorder) Save(user, claUser interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockService)(nil).Save), user, claUser) +} + +// SearchUsers mocks base method. +func (m *MockService) SearchUsers(field, searchTerm string, fullMatch bool) (*models.Users, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchUsers", field, searchTerm, fullMatch) + ret0, _ := ret[0].(*models.Users) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchUsers indicates an expected call of SearchUsers. +func (mr *MockServiceMockRecorder) SearchUsers(field, searchTerm, fullMatch interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchUsers", reflect.TypeOf((*MockService)(nil).SearchUsers), field, searchTerm, fullMatch) +} + +// UpdateUser mocks base method. +func (m *MockService) UpdateUser(userID string, updates map[string]interface{}) (*models.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUser", userID, updates) + ret0, _ := ret[0].(*models.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateUser indicates an expected call of UpdateUser. +func (mr *MockServiceMockRecorder) UpdateUser(userID, updates interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockService)(nil).UpdateUser), userID, updates) +} + +// UpdateUserCompanyID mocks base method. +func (m *MockService) UpdateUserCompanyID(userID, companyID, note string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUserCompanyID", userID, companyID, note) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateUserCompanyID indicates an expected call of UpdateUserCompanyID. +func (mr *MockServiceMockRecorder) UpdateUserCompanyID(userID, companyID, note interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserCompanyID", reflect.TypeOf((*MockService)(nil).UpdateUserCompanyID), userID, companyID, note) +} diff --git a/cla-backend-go/v2/signatures/service.go b/cla-backend-go/v2/signatures/service.go index 16bb8a8d2..581bd6433 100644 --- a/cla-backend-go/v2/signatures/service.go +++ b/cla-backend-go/v2/signatures/service.go @@ -9,88 +9,99 @@ import ( "errors" "fmt" + "github.com/communitybridge/easycla/cla-backend-go/project/service" + + "github.com/LF-Engineering/lfx-kit/auth" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/aws" + "github.com/communitybridge/easycla/cla-backend-go/events" "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" "github.com/jinzhu/copier" "github.com/communitybridge/easycla/cla-backend-go/company" - v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/models" + v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" + v2Sigs "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations/signatures" log "github.com/communitybridge/easycla/cla-backend-go/logging" - "github.com/communitybridge/easycla/cla-backend-go/project" "github.com/communitybridge/easycla/cla-backend-go/signatures" + "github.com/communitybridge/easycla/cla-backend-go/users" "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/communitybridge/easycla/cla-backend-go/v2/approvals" "github.com/sirupsen/logrus" ) // constants const ( - // used when we want to query all data from dependent service. - HugePageSize = int64(10000) - CclaSignatureType = "ccla" - ClaSignatureType = "cla" + // HugePageSize constant for querying signatures + HugePageSize = int64(10000) ) -// errors var ( + // ErrZipNotPresent error ErrZipNotPresent = errors.New("zip file not present") ) -type service struct { - v1ProjectService project.Service - v1CompanyService company.IService - v1SignatureService signatures.SignatureService - projectsClaGroupsRepo projects_cla_groups.Repository - s3 *s3.S3 - signaturesBucket string -} - -// Service contains method of v2 signature service -type Service interface { - GetProjectCompanySignatures(ctx context.Context, companySFID string, projectSFID string) (*models.Signatures, error) +// ServiceInterface contains method of v2 signature service +type ServiceInterface interface { + GetProjectCompanySignatures(ctx context.Context, companyID, companySFID, projectSFID string) (*models.CorporateSignatures, error) GetProjectIclaSignaturesCsv(ctx context.Context, claGroupID string) ([]byte, error) GetProjectCclaSignaturesCsv(ctx context.Context, claGroupID string) ([]byte, error) - GetProjectIclaSignatures(ctx context.Context, claGroupID string, searchTerm *string) (*models.IclaSignatures, error) - GetClaGroupCorporateContributorsCsv(ctx context.Context, claGroupID string, companySFID string) ([]byte, error) - GetClaGroupCorporateContributors(ctx context.Context, claGroupID string, companySFID *string, searchTerm *string) (*models.CorporateContributorList, error) + GetProjectIclaSignatures(ctx context.Context, claGroupID string, searchTerm *string, approved, signed *bool, pageSize int64, nextKey string, withExtraDetails bool) (*models.IclaSignatures, error) + GetClaGroupCorporateContributorsCsv(ctx context.Context, claGroupID string, companyID string) ([]byte, error) + GetClaGroupCorporateContributors(ctx context.Context, params v2Sigs.ListClaGroupCorporateContributorsParams) (*models.CorporateContributorList, error) GetSignedDocument(ctx context.Context, signatureID string) (*models.SignedDocument, error) GetSignedIclaZipPdf(claGroupID string) (*models.URLObject, error) GetSignedCclaZipPdf(claGroupID string) (*models.URLObject, error) + InvalidateICLA(ctx context.Context, claGroupID string, userID string, authUser *auth.User, eventsService events.Service, eventArgs *events.LogEventArgs) error + EclaAutoCreate(ctx context.Context, signatureID string, autoCreateECLA bool) error + IsUserAuthorized(ctx context.Context, lfid, claGroupId string) (*models.LfidAuthorizedResponse, error) +} + +// Service structure/model +type Service struct { + v1ProjectService service.Service + v1CompanyService company.IService + v1SignatureService signatures.SignatureService + v1SignatureRepo signatures.SignatureRepository + usersService users.Service + projectsClaGroupsRepo projects_cla_groups.Repository + s3 *s3.S3 + signaturesBucket string + approvalsRepos approvals.IRepository } // NewService creates instance of v2 signature service -func NewService(awsSession *session.Session, signaturesBucketName string, v1ProjectService project.Service, +func NewService(awsSession *session.Session, signaturesBucketName string, v1ProjectService service.Service, v1CompanyService company.IService, v1SignatureService signatures.SignatureService, - pcgRepo projects_cla_groups.Repository) *service { - return &service{ + pcgRepo projects_cla_groups.Repository, v1SignatureRepo signatures.SignatureRepository, usersService users.Service, approvalsRepo approvals.IRepository) *Service { + return &Service{ v1ProjectService: v1ProjectService, v1CompanyService: v1CompanyService, v1SignatureService: v1SignatureService, + v1SignatureRepo: v1SignatureRepo, + usersService: usersService, projectsClaGroupsRepo: pcgRepo, s3: s3.New(awsSession), signaturesBucket: signaturesBucketName, + approvalsRepos: approvalsRepo, } } -func (s *service) GetProjectCompanySignatures(ctx context.Context, companySFID string, projectSFID string) (*models.Signatures, error) { - companyModel, err := s.v1CompanyService.GetCompanyByExternalID(ctx, companySFID) - if err != nil { - return nil, err - } - pm, err := s.projectsClaGroupsRepo.GetClaGroupIDForProject(projectSFID) +// GetProjectCompanySignatures return the signatures for the specified project and company information +func (s *Service) GetProjectCompanySignatures(ctx context.Context, companyID, companySFID, projectSFID string) (*models.CorporateSignatures, error) { + pm, err := s.projectsClaGroupsRepo.GetClaGroupIDForProject(ctx, projectSFID) if err != nil { return nil, err } signed := true approved := true - sig, err := s.v1SignatureService.GetProjectCompanySignature(ctx, companyModel.CompanyID, pm.ClaGroupID, &signed, &approved, nil, aws.Int64(HugePageSize)) + sig, err := s.v1SignatureService.GetProjectCompanySignature(ctx, companyID, pm.ClaGroupID, &signed, &approved, nil, aws.Int64(HugePageSize)) if err != nil { return nil, err } @@ -99,11 +110,19 @@ func (s *service) GetProjectCompanySignatures(ctx context.Context, companySFID s } if sig != nil { resp.ResultCount = 1 + resp.TotalCount = 1 + resp.ProjectID = sig.ProjectID resp.Signatures = append(resp.Signatures, sig) } - return v2SignaturesReplaceCompanyID(resp, companyModel.CompanyID, companySFID) + oldformatSignatures, err := v2SignaturesReplaceCompanyID(resp, companyID, companySFID) + if err != nil { + return nil, err + } + + return s.v2SignaturesToCorporateSignatures(*oldformatSignatures, projectSFID) } +// eclaSigCsvLine returns a single ECLA signature CSV line func eclaSigCsvLine(sig *v1Models.CorporateContributor) string { var dateTime string t, err := utils.ParseDateTime(sig.Timestamp) @@ -116,14 +135,10 @@ func eclaSigCsvLine(sig *v1Models.CorporateContributor) string { return fmt.Sprintf("\n%s,%s,%s,%s,\"%s\"", sig.GithubID, sig.LinuxFoundationID, sig.Name, sig.Email, dateTime) } -func (s service) GetClaGroupCorporateContributorsCsv(ctx context.Context, claGroupID string, companySFID string) ([]byte, error) { +// GetClaGroupCorporateContributorsCsv returns the CLA Group corporate contributors as a CSV +func (s *Service) GetClaGroupCorporateContributorsCsv(ctx context.Context, claGroupID string, companyID string) ([]byte, error) { var b bytes.Buffer - comp, companyErr := s.v1CompanyService.GetCompanyByExternalID(ctx, companySFID) - if companyErr != nil { - return nil, companyErr - } - - result, err := s.v1SignatureService.GetClaGroupCorporateContributors(ctx, claGroupID, &comp.CompanyID, nil) + result, err := s.v1SignatureService.GetClaGroupCorporateContributors(ctx, claGroupID, &companyID, nil, nil, nil) if err != nil { return nil, err } @@ -132,34 +147,58 @@ func (s service) GetClaGroupCorporateContributorsCsv(ctx context.Context, claGro return nil, errors.New("not Found") } - b.WriteString(`Github ID,LF_ID,Name,Email,Date Signed`) + b.WriteString(`GitHub ID,LF_ID,Name,Email,Date Signed`) for _, sig := range result.List { b.WriteString(eclaSigCsvLine(sig)) } return b.Bytes(), nil } -func (s service) GetProjectIclaSignaturesCsv(ctx context.Context, claGroupID string) ([]byte, error) { - var b bytes.Buffer - result, err := s.v1SignatureService.GetClaGroupICLASignatures(ctx, claGroupID, nil) - if err != nil { - return nil, err +// GetProjectIclaSignaturesCsv returns the ICLA signatures as a CSV file for the specified CLA Group +func (s *Service) GetProjectIclaSignaturesCsv(ctx context.Context, claGroupID string) ([]byte, error) { + f := logrus.Fields{ + "functionName": "v2.signature_service.GetProjectIclaSignaturesCsv", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupID, } - b.WriteString(`Github ID,LF_ID,Name,Email,Date Signed`) - for _, sig := range result.List { + + var totalResults []*v1Models.IclaSignature + lastKeyScanned := "" + batchSize := int64(500) + loadUserDetails := true + // Loop until we have all the results - 100 per page + for { + log.WithFields(f).Debugf("loading ICLAs - %d of %d so far - requesting page with lastKeyScanned: %s", batchSize, len(totalResults), lastKeyScanned) + result, err := s.v1SignatureService.GetClaGroupICLASignatures(ctx, claGroupID, nil, nil, nil, batchSize, lastKeyScanned, loadUserDetails) + if err != nil { + return nil, err + } + totalResults = append(totalResults, result.List...) + + if result.LastKeyScanned == "" { + break + } + + lastKeyScanned = result.LastKeyScanned + } + + var b bytes.Buffer + b.WriteString(iclaSigCsvHeader()) + for _, sig := range totalResults { b.WriteString(iclaSigCsvLine(sig)) } return b.Bytes(), nil } -func (s service) GetProjectCclaSignaturesCsv(ctx context.Context, claGroupID string) ([]byte, error) { +// GetProjectCclaSignaturesCsv returns the ICLA signatures as a CSV file for the specified CLA Group and search term filters +func (s *Service) GetProjectCclaSignaturesCsv(ctx context.Context, claGroupID string) ([]byte, error) { f := logrus.Fields{ - "functionName": "GetProjectCclaSignaturesCsv", + "functionName": "v2.signatures.service.GetProjectCclaSignaturesCsv", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": claGroupID, } log.WithFields(f).Debug("querying for CCLA signatures...") - result, err := s.v1SignatureService.GetClaGroupCCLASignatures(ctx, claGroupID) + result, err := s.v1SignatureService.GetClaGroupCCLASignatures(ctx, claGroupID, nil, nil) if err != nil { log.WithFields(f).Warnf("error loading CCLA signatures for CLA group, error: %+v", err) return nil, err @@ -176,33 +215,49 @@ func (s service) GetProjectCclaSignaturesCsv(ctx context.Context, claGroupID str return b.Bytes(), nil } -func (s service) GetProjectIclaSignatures(ctx context.Context, claGroupID string, searchTerm *string) (*models.IclaSignatures, error) { +// GetProjectIclaSignatures returns the ICLA signatures for the specified CLA Group and search term filters +func (s *Service) GetProjectIclaSignatures(ctx context.Context, claGroupID string, searchTerm *string, approved, signed *bool, pageSize int64, nextKey string, withExtraDetails bool) (*models.IclaSignatures, error) { + f := logrus.Fields{ + "functionName": "v2.signatures.service.GetProjectIclaSignatures", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupID, + "searchTerm": utils.StringValue(searchTerm), + "approved": utils.BoolValue(approved), + "signed": utils.BoolValue(signed), + "withExtraDetails": withExtraDetails, + } + var out models.IclaSignatures - result, err := s.v1SignatureService.GetClaGroupICLASignatures(ctx, claGroupID, searchTerm) + result, err := s.v1SignatureService.GetClaGroupICLASignatures(ctx, claGroupID, searchTerm, approved, signed, pageSize, nextKey, withExtraDetails) if err != nil { + log.WithFields(f).WithError(err).Warn("unable to load ICLA signatures using the specified search parameters") return nil, err } + err = copier.Copy(&out, result) if err != nil { + log.WithFields(f).WithError(err).Warn("unable to convert signature results from v1 to v2") return nil, err } + return &out, nil } -func (s service) GetSignedDocument(ctx context.Context, signatureID string) (*models.SignedDocument, error) { +// GetSignedDocument returns the signed document for the specified signature ID +func (s *Service) GetSignedDocument(ctx context.Context, signatureID string) (*models.SignedDocument, error) { sig, err := s.v1SignatureService.GetSignature(ctx, signatureID) if err != nil { return nil, err } - if sig.SignatureType == ClaSignatureType && sig.CompanyName != "" { + if sig.SignatureType == utils.SignatureTypeCLA && sig.CompanyName != "" { return nil, errors.New("bad request. employee signature does not have signed document") } var url string switch sig.SignatureType { - case ClaSignatureType: - url = utils.SignedCLAFilename(sig.ProjectID, "icla", sig.SignatureReferenceID.String(), sig.SignatureID.String()) - case CclaSignatureType: - url = utils.SignedCLAFilename(sig.ProjectID, "ccla", sig.SignatureReferenceID.String(), sig.SignatureID.String()) + case utils.SignatureTypeCLA: + url = utils.SignedCLAFilename(sig.ProjectID, "icla", sig.SignatureReferenceID, sig.SignatureID) + case utils.SignatureTypeCCLA: + url = utils.SignedCLAFilename(sig.ProjectID, "ccla", sig.SignatureReferenceID, sig.SignatureID) } signedURL, err := utils.GetDownloadLink(url) if err != nil { @@ -214,8 +269,9 @@ func (s service) GetSignedDocument(ctx context.Context, signatureID string) (*mo }, nil } -func (s service) GetSignedCclaZipPdf(claGroupID string) (*models.URLObject, error) { - url := utils.SignedClaGroupZipFilename(claGroupID, CCLA) +// GetSignedCclaZipPdf returns the signed CCLA Zip PDF reference +func (s *Service) GetSignedCclaZipPdf(claGroupID string) (*models.URLObject, error) { + url := utils.SignedClaGroupZipFilename(claGroupID, utils.ClaTypeCCLA) ok, err := s.IsZipPresentOnS3(url) if err != nil { return nil, err @@ -232,8 +288,9 @@ func (s service) GetSignedCclaZipPdf(claGroupID string) (*models.URLObject, erro }, nil } -func (s service) GetSignedIclaZipPdf(claGroupID string) (*models.URLObject, error) { - url := utils.SignedClaGroupZipFilename(claGroupID, ICLA) +// GetSignedIclaZipPdf returns the signed ICLA Zip PDF reference +func (s *Service) GetSignedIclaZipPdf(claGroupID string) (*models.URLObject, error) { + url := utils.SignedClaGroupZipFilename(claGroupID, utils.ClaTypeICLA) ok, err := s.IsZipPresentOnS3(url) if err != nil { return nil, err @@ -250,7 +307,8 @@ func (s service) GetSignedIclaZipPdf(claGroupID string) (*models.URLObject, erro }, nil } -func (s service) IsZipPresentOnS3(zipFilePath string) (bool, error) { +// IsZipPresentOnS3 returns true if the specified file is present in S3 +func (s *Service) IsZipPresentOnS3(zipFilePath string) (bool, error) { _, err := s.s3.GetObject(&s3.GetObjectInput{ Bucket: aws.String(s.signaturesBucket), Key: aws.String(zipFilePath), @@ -265,40 +323,216 @@ func (s service) IsZipPresentOnS3(zipFilePath string) (bool, error) { return true, nil } -func (s service) GetClaGroupCorporateContributors(ctx context.Context, claGroupID string, companySFID *string, searchTerm *string) (*models.CorporateContributorList, error) { +// GetClaGroupCorporateContributors returns the list of corporate contributors for the specified CLA Group and company +func (s *Service) GetClaGroupCorporateContributors(ctx context.Context, params v2Sigs.ListClaGroupCorporateContributorsParams) (*models.CorporateContributorList, error) { + f := logrus.Fields{ + "functionName": "v2.signatures.service.GetClaGroupCorporateContributors", + "claGroupID": params.ClaGroupID, + "companyID": params.CompanyID, + } + if params.SearchTerm != nil { + f["searchTerm"] = *params.SearchTerm + } + + log.WithFields(f).Debug("querying CLA corporate contributors...") + result, err := s.v1SignatureService.GetClaGroupCorporateContributors(ctx, params.ClaGroupID, params.CompanyID, params.PageSize, params.NextKey, params.SearchTerm) + if err != nil { + return nil, err + } + log.WithFields(f).Debugf("discovered %d CLA corporate contributors...", len(result.List)) + + log.WithFields(f).Debug("converting to v2 response model...") + var resp models.CorporateContributorList + err = copier.Copy(&resp, result) + if err != nil { + return nil, err + } + + return &resp, nil +} + +// InvalidateICLA invalidates the specified signature record using the supplied parameters +func (s *Service) InvalidateICLA(ctx context.Context, claGroupID string, userID string, authUser *auth.User, eventsService events.Service, eventArgs *events.LogEventArgs) error { f := logrus.Fields{ - "functionName": "GetClaGroupCorporateContributors", + "functionName": "v2.signatures.service.InvalidateICLA", "claGroupID": claGroupID, + "userID": userID, } - if companySFID != nil { - f["companySFID"] = *companySFID + // Get signature record + log.WithFields(f).Debug("getting signature record ...") + approved, signed := true, true + icla, iclaErr := s.v1SignatureService.GetIndividualSignature(ctx, claGroupID, userID, &approved, &signed) + if iclaErr != nil { + log.WithFields(f).Debug("unable to get individual signature") + return iclaErr } - if searchTerm != nil { - f["searchTerm"] = *searchTerm + + // Get cla Group + log.WithFields(f).Debug("getting clGroup...") + claGroup, claGrpErr := s.v1ProjectService.GetCLAGroupByID(ctx, claGroupID) + if claGrpErr != nil { + log.WithFields(f).Debug("unable to fetch cla Group record") + return claGrpErr } - var companyID *string - if companySFID != nil { - log.WithFields(f).Debug("loading company by companySFID...") - companyModel, err := s.v1CompanyService.GetCompanyByExternalID(ctx, *companySFID) + //Get user record + user, userErr := s.usersService.GetUser(userID) + if userErr != nil { + log.WithFields(f).Debug("unable to get user record") + return userErr + } + + log.WithFields(f).Debug("invalidating signature record ...") + note := fmt.Sprintf("Signature invalidated (approved set to false) by %s for %s ", authUser.UserName, utils.GetBestUsername(user)) + err := s.v1SignatureRepo.InvalidateProjectRecord(ctx, icla.SignatureID, note) + if err != nil { + log.WithFields(f).Debug("unable to invalidate icla record") + return err + } + // send email + email := utils.GetBestEmail(user) + log.WithFields(f).Debugf("sending invalidation email to : %s ", email) + subject := fmt.Sprintf("EasyCLA: ICLA invalidated for %s ", claGroup.ProjectName) + params := signatures.InvalidateSignatureTemplateParams{ + RecipientName: utils.GetBestUsername(user), + ProjectManager: authUser.UserName, + CLAGroupName: claGroup.ProjectName, + } + body, renderErr := utils.RenderTemplate(claGroup.Version, signatures.InvalidateICLASignatureTemplateName, signatures.InvalidateICLASignatureTemplate, params) + if renderErr != nil { + log.WithFields(f).Debugf("unable to render email approval template for user: %s ", email) + } else { + err := utils.SendEmail(subject, body, []string{email}) if err != nil { - return nil, err + log.WithFields(f).Debugf("unable to send approval list update email to : %s ", email) } - companyID = &companyModel.CompanyID } - log.WithFields(f).Debug("querying CLA corporate contributors...") - result, err := s.v1SignatureService.GetClaGroupCorporateContributors(ctx, claGroupID, companyID, searchTerm) + eventArgs.UserName = utils.GetBestUsername(user) + eventArgs.UserModel = user + eventArgs.ProjectName = claGroup.ProjectName + + // Log event + eventsService.LogEventWithContext(ctx, eventArgs) + + return nil +} + +// EclaAutoCreate this routine updates the CCLA signature record by adjusting the auto_create_ecla column to the specified value +func (s *Service) EclaAutoCreate(ctx context.Context, signatureID string, autoCreateECLA bool) error { + f := logrus.Fields{ + "functionName": "v2.signatures.service.EclaAutoCreate", + "signatureID": signatureID, + "autoCreateECLA": autoCreateECLA, + } + + log.WithFields(f).Debug("updating CCLA signature record for auto_create_ecla...") + err := s.v1SignatureRepo.EclaAutoCreate(ctx, signatureID, autoCreateECLA) if err != nil { + log.WithFields(f).Debug("unable to update CCLA signature record for auto_create_ecla") + return err + } + + return nil +} + +func (s *Service) IsUserAuthorized(ctx context.Context, lfid, claGroupId string) (*models.LfidAuthorizedResponse, error) { + f := logrus.Fields{ + "functionName": "v2.signatures.service.IsUserAuthorized", + "lfid": lfid, + "claGroupId": claGroupId, + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + hasSigned := false + + response := models.LfidAuthorizedResponse{ + ClaGroupID: claGroupId, + Lfid: lfid, + Authorized: false, + ICLA: false, + CCLA: false, + CCLARequiresICLA: false, + CompanyAffiliation: false, + } + + // fetch cla group + log.WithFields(f).Debug("fetching cla group") + claGroup, err := s.v1ProjectService.GetCLAGroupByID(ctx, claGroupId) + if err != nil { + log.WithFields(f).WithError(err).Debug("unable to fetch cla group") return nil, err } - log.WithFields(f).Debug("converting to v2 response model...") - var resp models.CorporateContributorList - err = copier.Copy(&resp, result) + if claGroup == nil { + log.WithFields(f).Debug("cla group not found") + return &response, nil + } + response.CCLARequiresICLA = claGroup.ProjectCCLARequiresICLA + + // fetch cla user + log.WithFields(f).Debug("fetching user by lfid") + user, err := s.usersService.GetUserByLFUserName(lfid) + if err != nil { + log.WithFields(f).WithError(err).Debug("unable to fetch lfusername") return nil, err } - return &resp, nil + if user == nil { + log.WithFields(f).Debug("user not found") + return &response, nil + } + + // check if user has signed ICLA + log.WithFields(f).Debug("checking if user has signed ICLA") + approved, signed := true, true + icla, iclaErr := s.v1SignatureService.GetIndividualSignature(ctx, claGroupId, user.UserID, &approved, &signed) + if iclaErr != nil { + log.WithFields(f).WithError(iclaErr).Debug("unable to get individual signature") + } + + if icla != nil { + log.WithFields(f).Debug("user has signed ICLA") + response.ICLA = true + hasSigned = true + } else { + log.WithFields(f).Debug("user has not signed ICLA") + } + + // fetch company + if user.CompanyID == "" { + log.WithFields(f).Debug("user company id not found") + response.CompanyAffiliation = false + } else { + log.WithFields(f).Debug("fetching company") + companyModel, err := s.v1CompanyService.GetCompany(ctx, user.CompanyID) + if companyErr, ok := err.(*utils.CompanyNotFound); ok { + log.WithFields(f).WithError(companyErr).Debug("company not found") + response.CompanyAffiliation = false + } else if err != nil { + log.WithFields(f).WithError(err).Debug("unable to fetch company") + return nil, err + } else { + log.WithFields(f).Debug("company found") + response.CompanyAffiliation = true + // process ecla + ecla, err := s.v1SignatureService.ProcessEmployeeSignature(ctx, companyModel, claGroup, user) + if err != nil { + log.WithFields(f).WithError(err).Debug("unable to process ecla") + return nil, err + } + log.WithFields(f).Debugf("ecla value: %b", ecla) + if ecla != nil && *ecla { + log.WithFields(f).Debug("user has signed ECLA") + hasSigned = true + response.CCLA = true + } else { + log.WithFields(f).Debug("user has not acknowledged with the company ") + } + } + } + + response.Authorized = hasSigned + return &response, nil } diff --git a/cla-backend-go/v2/signatures/service_test.go b/cla-backend-go/v2/signatures/service_test.go new file mode 100644 index 000000000..b65885db2 --- /dev/null +++ b/cla-backend-go/v2/signatures/service_test.go @@ -0,0 +1,289 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package signatures + +import ( + "context" + "errors" + "testing" + + 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" + + // mock_signatures "github.com/communitybridge/easycla/cla-backend-go/v2/signatures/mock_v1_signatures" + mock_company "github.com/communitybridge/easycla/cla-backend-go/company/mocks" + ini "github.com/communitybridge/easycla/cla-backend-go/init" + mock_project "github.com/communitybridge/easycla/cla-backend-go/project/mocks" + mock_v1_signatures "github.com/communitybridge/easycla/cla-backend-go/signatures/mocks" + mock_users "github.com/communitybridge/easycla/cla-backend-go/v2/signatures/mock_users" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +func TestService_IsUserAuthorized(t *testing.T) { + type testCase struct { + name string + lfid string + projectID string + userID string + companyID string + getUserByLFUsernameResult *v1Models.User + getUserByLFUsernameError error + claGroupRequiresICLA bool + getIndividualSignatureResult *v1Models.Signature + getIndividualSignatureError error + processEmployeeSignatureResult *bool + processEmployeeSignatureError error + expectedAuthorized bool + expectedCCLARequiresICLA bool + expectedICLA bool + expectedCCLA bool + expectedCompanyAffiliation bool + getCompanyResult *v1Models.Company + getCompanyError error + } + + cases := []testCase{ + { + name: "claGroupRequiresICLA", + lfid: "foobar_1", + projectID: "project-123", + userID: "user-123", + companyID: "company-123", + claGroupRequiresICLA: true, + getUserByLFUsernameResult: &v1Models.User{ + UserID: "user-123", + CompanyID: "company-123", + LfUsername: "foobar_1", + }, + getUserByLFUsernameError: nil, + getIndividualSignatureResult: &v1Models.Signature{ + SignatureID: "signature-123", + }, + getIndividualSignatureError: nil, + processEmployeeSignatureResult: func() *bool { b := true; return &b }(), + processEmployeeSignatureError: nil, + expectedAuthorized: true, + expectedCCLARequiresICLA: true, + expectedICLA: true, + expectedCCLA: true, + expectedCompanyAffiliation: true, + getCompanyResult: &v1Models.Company{ + CompanyID: "company-123", + }, + getCompanyError: nil, + }, + { + name: "claGroupDoesNotRequireICLA", + lfid: "foobar_2", + projectID: "project-123", + userID: "user-123", + companyID: "company-123", + claGroupRequiresICLA: false, + getUserByLFUsernameResult: &v1Models.User{ + UserID: "user-123", + CompanyID: "company-123", + LfUsername: "foobar_2", + }, + getUserByLFUsernameError: nil, + getIndividualSignatureResult: &v1Models.Signature{ + SignatureID: "signature-123", + }, + getIndividualSignatureError: nil, + processEmployeeSignatureResult: func() *bool { b := true; return &b }(), + processEmployeeSignatureError: nil, + expectedAuthorized: true, + expectedCCLARequiresICLA: false, + expectedICLA: true, + expectedCCLA: true, + expectedCompanyAffiliation: true, + getCompanyResult: &v1Models.Company{ + CompanyID: "company-123", + }, + getCompanyError: nil, + }, + { + name: "icla signature found", + lfid: "foobar_3", + projectID: "project-123", + userID: "user-123", + companyID: "company-123", + getUserByLFUsernameResult: &v1Models.User{ + UserID: "user-123", + CompanyID: "company-123", + LfUsername: "foobar_3", + }, + getUserByLFUsernameError: nil, + claGroupRequiresICLA: true, + getIndividualSignatureResult: &v1Models.Signature{ + SignatureID: "signature-123", + }, + getIndividualSignatureError: nil, + processEmployeeSignatureResult: nil, + processEmployeeSignatureError: nil, + expectedAuthorized: true, + expectedCCLARequiresICLA: true, + expectedICLA: true, + expectedCCLA: false, + expectedCompanyAffiliation: true, + getCompanyResult: &v1Models.Company{ + CompanyID: "company-123", + }, + getCompanyError: nil, + }, + { + name: "icla signature not found", + lfid: "foobar_4", + projectID: "project-123", + userID: "user-123", + companyID: "company-123", + getUserByLFUsernameResult: &v1Models.User{ + UserID: "user-123", + CompanyID: "company-123", + LfUsername: "foobar_4", + }, + getUserByLFUsernameError: nil, + claGroupRequiresICLA: true, + getIndividualSignatureResult: nil, + getIndividualSignatureError: errors.New("some error"), + processEmployeeSignatureResult: func() *bool { b := true; return &b }(), + processEmployeeSignatureError: nil, + expectedAuthorized: true, + expectedCCLARequiresICLA: true, + expectedICLA: false, + expectedCCLA: true, + expectedCompanyAffiliation: true, + getCompanyResult: &v1Models.Company{ + CompanyID: "company-123", + }, + getCompanyError: nil, + }, + { + name: "individual signature error", + lfid: "foobar_5", + projectID: "project-123", + userID: "user-123", + companyID: "company-123", + getUserByLFUsernameResult: &v1Models.User{ + UserID: "user-123", + CompanyID: "company-123", + }, + getUserByLFUsernameError: nil, + claGroupRequiresICLA: true, + getIndividualSignatureResult: nil, + getIndividualSignatureError: errors.New("some error"), + processEmployeeSignatureResult: func() *bool { b := false; return &b }(), + processEmployeeSignatureError: nil, + expectedAuthorized: false, + expectedCCLARequiresICLA: true, + expectedICLA: false, + expectedCCLA: false, + expectedCompanyAffiliation: true, + getCompanyResult: &v1Models.Company{ + CompanyID: "company-123", + }, + getCompanyError: nil, + }, + { + name: "user has not signed ccla and icla", + lfid: "foobar_6", + projectID: "project-123", + userID: "user-123", + companyID: "company-123", + getUserByLFUsernameResult: nil, + getUserByLFUsernameError: nil, + claGroupRequiresICLA: true, + expectedAuthorized: false, + expectedCCLARequiresICLA: true, + expectedICLA: false, + expectedCCLA: false, + expectedCompanyAffiliation: false, + getCompanyResult: &v1Models.Company{ + CompanyID: "company-123", + }, + getCompanyError: nil, + }, + { + name: "user has icla and has company id that does not exist", + lfid: "foobar_7", + projectID: "project-123", + userID: "user-123", + companyID: "company-123", + getUserByLFUsernameResult: &v1Models.User{ + UserID: "user-123", + CompanyID: "company-123", + }, + getUserByLFUsernameError: nil, + claGroupRequiresICLA: false, + expectedAuthorized: true, + expectedCCLARequiresICLA: false, + expectedICLA: true, + expectedCCLA: false, + expectedCompanyAffiliation: false, + getCompanyResult: nil, + getCompanyError: &utils.CompanyNotFound{ + Message: "no company matching company record", + CompanyID: "company-123", + }, + getIndividualSignatureResult: &v1Models.Signature{ + SignatureID: "signature-123", + }, + getIndividualSignatureError: nil, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + var err error + var result *models.LfidAuthorizedResponse + + awsSession, err := ini.GetAWSSession() + if err != nil { + assert.Fail(t, "unable to create AWS session") + } + + mockProjectService := mock_project.NewMockService(ctrl) + mockProjectService.EXPECT().GetCLAGroupByID(context.Background(), tc.projectID).Return(&v1Models.ClaGroup{ + ProjectID: tc.projectID, + ProjectCCLARequiresICLA: tc.claGroupRequiresICLA, + }, nil) + + mockUserService := mock_users.NewMockService(ctrl) + mockUserService.EXPECT().GetUserByLFUserName(tc.lfid).Return(tc.getUserByLFUsernameResult, tc.getUserByLFUsernameError) + + if tc.getUserByLFUsernameResult != nil { + mockSignatureService := mock_v1_signatures.NewMockSignatureService(ctrl) + + approved := true + signed := true + mockSignatureService.EXPECT().GetIndividualSignature(context.Background(), tc.projectID, tc.userID, &approved, &signed).Return(tc.getIndividualSignatureResult, tc.getIndividualSignatureError) + + if tc.getCompanyError == nil { + mockSignatureService.EXPECT().ProcessEmployeeSignature(context.Background(), gomock.Any(), gomock.Any(), gomock.Any()).Return(tc.processEmployeeSignatureResult, tc.processEmployeeSignatureError) + } + + mockCompanyService := mock_company.NewMockIService(ctrl) + mockCompanyService.EXPECT().GetCompany(context.Background(), tc.companyID).Return(tc.getCompanyResult, tc.getCompanyError) + + service := NewService(awsSession, "", mockProjectService, mockCompanyService, mockSignatureService, nil, nil, mockUserService, nil) + + result, err = service.IsUserAuthorized(context.Background(), tc.lfid, tc.projectID) + + } else { + service := NewService(awsSession, "", mockProjectService, nil, nil, nil, nil, mockUserService, nil) + result, err = service.IsUserAuthorized(context.Background(), tc.lfid, tc.projectID) + } + assert.Nil(t, err) + assert.Equal(t, tc.expectedAuthorized, result.Authorized) + assert.Equal(t, tc.expectedCCLARequiresICLA, result.CCLARequiresICLA) + assert.Equal(t, tc.expectedICLA, result.ICLA) + assert.Equal(t, tc.expectedCCLA, result.CCLA) + assert.Equal(t, tc.expectedCompanyAffiliation, result.CompanyAffiliation) + }) + } +} diff --git a/cla-backend-go/v2/signatures/validators.go b/cla-backend-go/v2/signatures/validators.go index c34d7bad3..862abcf66 100644 --- a/cla-backend-go/v2/signatures/validators.go +++ b/cla-backend-go/v2/signatures/validators.go @@ -31,7 +31,9 @@ func hasApprovalListUpdates(params signatures.UpdateApprovalListParams) bool { if len(params.Body.AddEmailApprovalList) > 0 || len(params.Body.RemoveEmailApprovalList) > 0 || len(params.Body.AddDomainApprovalList) > 0 || len(params.Body.RemoveDomainApprovalList) > 0 || len(params.Body.AddGithubUsernameApprovalList) > 0 || len(params.Body.RemoveGithubUsernameApprovalList) > 0 || - len(params.Body.AddGithubOrgApprovalList) > 0 || len(params.Body.RemoveGithubOrgApprovalList) > 0 { + len(params.Body.AddGithubOrgApprovalList) > 0 || len(params.Body.RemoveGithubOrgApprovalList) > 0 || + len(params.Body.AddGitlabUsernameApprovalList) > 0 || len(params.Body.RemoveGitlabUsernameApprovalList) > 0 || + len(params.Body.AddGitlabOrgApprovalList) > 0 || len(params.Body.RemoveGitlabOrgApprovalList) > 0 { return true } @@ -58,21 +60,21 @@ func entriesAreValid(params signatures.UpdateApprovalListParams) (string, bool) // Ensure the domains are valid for _, domain := range params.Body.AddDomainApprovalList { - msg, valid := utils.ValidDomain(domain) + msg, valid := utils.ValidDomain(domain, true) if !valid { isValid = false listOfErrors = append(listOfErrors, fmt.Sprintf("invalid add approval list domain %s - %s", domain, msg)) } } for _, domain := range params.Body.RemoveDomainApprovalList { - msg, valid := utils.ValidDomain(domain) + msg, valid := utils.ValidDomain(domain, true) if !valid { isValid = false listOfErrors = append(listOfErrors, fmt.Sprintf("invalid remove approval list domain %s - %s", domain, msg)) } } - // Ensure the github usernames are valid + // Ensure the GitHub usernames are valid for _, githubUsername := range params.Body.AddGithubUsernameApprovalList { msg, valid := utils.ValidGitHubUsername(githubUsername) if !valid { @@ -88,7 +90,7 @@ func entriesAreValid(params signatures.UpdateApprovalListParams) (string, bool) } } - // Ensure the github Organization values are valid + // Ensure the GitHub Organization values are valid for _, githubOrg := range params.Body.AddGithubOrgApprovalList { msg, valid := utils.ValidGitHubOrg(githubOrg) if !valid { @@ -104,5 +106,37 @@ func entriesAreValid(params signatures.UpdateApprovalListParams) (string, bool) } } + // Ensure the Gitlab usernames are valid + for _, githubUsername := range params.Body.AddGitlabUsernameApprovalList { + msg, valid := utils.ValidGitlabUsername(githubUsername) + if !valid { + isValid = false + listOfErrors = append(listOfErrors, fmt.Sprintf("invalid add approval list Gitlab Username %s - %s", githubUsername, msg)) + } + } + for _, githubUsername := range params.Body.RemoveGitlabUsernameApprovalList { + msg, valid := utils.ValidGitlabUsername(githubUsername) + if !valid { + isValid = false + listOfErrors = append(listOfErrors, fmt.Sprintf("invalid remove approval list Gitlab Username %s - %s", githubUsername, msg)) + } + } + + // Ensure the Gitlab Organization values are valid + for _, githubOrg := range params.Body.AddGitlabOrgApprovalList { + msg, valid := utils.ValidGitlabOrg(githubOrg) + if !valid { + isValid = false + listOfErrors = append(listOfErrors, fmt.Sprintf("invalid add approval list Gitlab Org %s - %s", githubOrg, msg)) + } + } + for _, githubOrg := range params.Body.RemoveGitlabOrgApprovalList { + msg, valid := utils.ValidGitlabOrg(githubOrg) + if !valid { + isValid = false + listOfErrors = append(listOfErrors, fmt.Sprintf("invalid remove approval list Gitlab Org %s - %s", githubOrg, msg)) + } + } + return strings.Join(listOfErrors, ", "), isValid } diff --git a/cla-backend-go/v2/signatures/zip_builder.go b/cla-backend-go/v2/signatures/zip_builder.go index c8f90f33a..c144ac197 100644 --- a/cla-backend-go/v2/signatures/zip_builder.go +++ b/cla-backend-go/v2/signatures/zip_builder.go @@ -29,8 +29,6 @@ import ( // constants const ( - ICLA = "icla" - CCLA = "ccla" ParallelDownloader = 100 ) @@ -42,8 +40,11 @@ type Zipper struct { // ZipBuilder provides method to build ICLA/CCLA zip type ZipBuilder interface { - BuildICLAZip(claGroupID string) error - BuildCCLAZip(claGroupID string) error + BuildICLAPDFZip(claGroupID string) error + BuildCCLAPDFZip(claGroupID string) error + BuildICLACSVZip(claGroupID string) error + BuildCCLACSVZip(claGroupID string) error + BuildECLACSVZip(claGroupID string) error } // NewZipBuilder returns the ZipBuilder @@ -62,17 +63,32 @@ func s3ZipPrefix(claType string, claGroupID string) string { return fmt.Sprintf("contract-group/%s/%s/", claGroupID, claType) } -// BuildICLAZip builds icla pdfs zip for cla-group and upload it on s3 -func (z *Zipper) BuildICLAZip(claGroupID string) error { - return z.buildZip(ICLA, claGroupID) +// BuildICLAPDFZip builds ICLA pdfs zip for cla-group and upload it on s3 +func (z *Zipper) BuildICLAPDFZip(claGroupID string) error { + return z.buildPDFZip(utils.ClaTypeICLA, claGroupID) } -// BuildCCLAZip builds ccla pdfs zip for cla-group and upload it on s3 -func (z *Zipper) BuildCCLAZip(claGroupID string) error { - return z.buildZip(CCLA, claGroupID) +// BuildCCLAPDFZip builds CCLA pdfs zip for cla-group and upload it on s3 +func (z *Zipper) BuildCCLAPDFZip(claGroupID string) error { + return z.buildPDFZip(utils.ClaTypeCCLA, claGroupID) } -func (z *Zipper) buildZip(claType string, claGroupID string) error { +// BuildICLACSVZip builds ICLA csvs zip for cla-group and upload it to AWS s3 +func (z *Zipper) BuildICLACSVZip(claGroupID string) error { + return z.buildCSVZip(utils.ClaTypeICLA, claGroupID) +} + +// BuildCCLACSVZip builds CCLA csvs zip for cla-group and upload it to AWS s3 +func (z *Zipper) BuildCCLACSVZip(claGroupID string) error { + return z.buildCSVZip(utils.ClaTypeCCLA, claGroupID) +} + +// BuildECLACSVZip builds ECLA csvs zip for cla-group and upload it to AWS s3 +func (z *Zipper) BuildECLACSVZip(claGroupID string) error { + return z.buildCSVZip(utils.ClaTypeECLA, claGroupID) +} + +func (z *Zipper) buildPDFZip(claType string, claGroupID string) error { f := logrus.Fields{"cla_group_id": claGroupID, "cla_type": claType} // get zip file from s3 buff, err := z.getZipFileFromS3(claType, claGroupID) @@ -149,6 +165,12 @@ func (z *Zipper) buildZip(claType string, claGroupID string) error { } return nil } +func (z *Zipper) buildCSVZip(claType string, claGroupID string) error { + f := logrus.Fields{"cla_group_id": claGroupID, "cla_type": claType} + // TODO: DAD - requires query to the signatures table to get the list of signatures, then encode as CSV, then build a zip file, and upload to S3 + log.WithFields(f).Infof("building %s csv zip for cla-group: %s is currently not supported", claType, claGroupID) + return nil +} // FileContent contains file content of s3 file type FileContent struct { diff --git a/cla-backend-go/v2/store/repository.go b/cla-backend-go/v2/store/repository.go new file mode 100644 index 000000000..cebd30f38 --- /dev/null +++ b/cla-backend-go/v2/store/repository.go @@ -0,0 +1,206 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package store + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/sirupsen/logrus" + + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" + "github.com/communitybridge/easycla/cla-backend-go/utils" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" +) + +// DBStore represents DB Model for the store table +type DBStore struct { + Key string `dynamodbav:"key"` + Value string `dynamodbav:"value"` + Expire float64 `dynamodbav:"expire"` +} + +// Repository interface +type Repository interface { + SetActiveSignatureMetaData(ctx context.Context, key string, expire int64, value string) error + GetActiveSignatureMetaData(ctx context.Context, UserId string) (map[string]interface{}, error) + DeleteActiveSignatureMetaData(ctx context.Context, key string) error +} + +type repo struct { + stage string + dynamoDBClient *dynamodb.DynamoDB + storeTableName string +} + +// NewRepository initiates Store repository instance +func NewRepository(awsSession *session.Session, stage string) Repository { + return repo{ + stage: stage, + dynamoDBClient: dynamodb.New(awsSession), + storeTableName: fmt.Sprintf("cla-%s-store", stage), + } +} + +// GetActiveSignatureMetaData returns active signature meta data +func (r repo) GetActiveSignatureMetaData(ctx context.Context, userId string) (map[string]interface{}, error) { + f := logrus.Fields{ + "functionName": "v2.store.repository.GetActiveSignatureMetaData", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "userId": userId, + } + var metadata map[string]interface{} + + log.WithFields(f).Debugf("querying for user: %s", userId) + + key := fmt.Sprintf("active_signature:%s", userId) + + result, err := r.dynamoDBClient.GetItem(&dynamodb.GetItemInput{ + TableName: &r.storeTableName, + Key: map[string]*dynamodb.AttributeValue{ + "key": { + S: &key, + }, + }, + }) + + if err != nil { + log.WithFields(f).WithError(err).Warn("problem querying store table") + return metadata, err + } + + if result.Item == nil { + log.WithFields(f).Warn("no record found") + return metadata, nil + } + + var jsonStr string + + err = dynamodbattribute.Unmarshal(result.Item["value"], &jsonStr) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem unmarshalling store record") + return metadata, err + } + + formatJson := strings.ReplaceAll(jsonStr, "\\\"", "\"") + + formatJson = strings.Trim(formatJson, "\"") + + log.WithFields(f).Debugf("format: %s", formatJson) + + jsonErr := json.Unmarshal([]byte(formatJson), &metadata) + + if jsonErr != nil { + log.WithFields(f).WithError(jsonErr).Warn("problem unmarshalling json string for metadata") + return nil, jsonErr + } + + log.WithFields(f).Debugf("metadata: %+v", metadata) + return metadata, nil +} + +// func findDifferences(str1, str2 string) string { +// f := logrus.Fields{ +// "functionName": "findDifference", +// } +// var differences string + +// // Find the minimum length of the two strings +// minLength := len(str1) +// if len(str2) < minLength { +// minLength = len(str2) +// } + +// // Compare each character and append the differences to the result string +// for i := 0; i < minLength; i++ { +// if str1[i] != str2[i] { +// differences += string(str1[i]) + string(str2[i]) + " " +// log.WithFields(f).Debugf("%s and %s", string(str1[i]), string(str2[i])) +// } +// } + +// // If the strings have different lengths, append the remaining characters +// if len(str1) > len(str2) { +// differences += str1[minLength:] +// } else if len(str2) > len(str1) { +// differences += str2[minLength:] +// } + +// return differences +// } + +// SetActiveSignatureMetaData sets active signature meta data +func (r repo) SetActiveSignatureMetaData(ctx context.Context, key string, expire int64, value string) error { + f := logrus.Fields{ + "functionName": "v2.store.repository.SetActiveSignatureMetaData", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "key": key, + "value": value, + "expire": expire, + } + + store := DBStore{ + Key: key, + Value: value, + Expire: float64(expire), + } + + log.WithFields(f).Debugf("key: %s ", store.Key) + log.WithFields(f).Debugf("value: %+s ", store.Value) + + v, err := dynamodbattribute.MarshalMap(store) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem marshalling store record") + return err + } + + log.WithFields(f).Debugf("Marshalled values: %+v", v) + + _, err = r.dynamoDBClient.PutItem(&dynamodb.PutItemInput{ + Item: v, + TableName: &r.storeTableName, + }) + + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to save store record") + return err + } + + log.WithFields(f).Debugf("Signature meta record data saved: %+v ", store) + + return nil +} + +func (r repo) DeleteActiveSignatureMetaData(ctx context.Context, key string) error { + f := logrus.Fields{ + "functionName": "v2.store.repository.DeleteActiveSignatureMetaData", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "key": key, + } + + log.WithFields(f).Debugf("key: %s ", key) + + _, err := r.dynamoDBClient.DeleteItem(&dynamodb.DeleteItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + "key": { + S: &key, + }, + }, + TableName: &r.storeTableName, + }) + + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to delete store record") + return err + } + + log.WithFields(f).Debugf("Signature meta record data deleted: %+v ", key) + + return nil +} diff --git a/cla-backend-go/v2/template/handlers.go b/cla-backend-go/v2/template/handlers.go index 7a88e18ae..55c995e3b 100644 --- a/cla-backend-go/v2/template/handlers.go +++ b/cla-backend-go/v2/template/handlers.go @@ -5,14 +5,18 @@ package template import ( "context" + "fmt" "net/http" + "strings" + + v1ProjectsCLAGroups "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" "github.com/sirupsen/logrus" "github.com/LF-Engineering/lfx-kit/auth" "github.com/communitybridge/easycla/cla-backend-go/events" v1Events "github.com/communitybridge/easycla/cla-backend-go/events" - v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/models" + 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/gen/v2/restapi/operations" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations/template" @@ -25,14 +29,14 @@ import ( ) // Configure API call -func Configure(api *operations.EasyclaAPI, service v1Template.Service, eventsService v1Events.Service) { +func Configure(api *operations.EasyclaAPI, service v1Template.ServiceInterface, v1ProjectClaGroupService v1ProjectsCLAGroups.Service, eventsService v1Events.Service) { // Retrieve a list of available templates api.TemplateGetTemplatesHandler = template.GetTemplatesHandlerFunc(func(params template.GetTemplatesParams, user *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(params.HTTPRequest.Context(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(user, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "TemplateGetTemplatesHandler", + "functionName": "v2.template.handlers.TemplateGetTemplatesHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), } @@ -50,33 +54,70 @@ func Configure(api *operations.EasyclaAPI, service v1Template.Service, eventsSer return template.NewGetTemplatesOK().WithPayload(response) }) - api.TemplateCreateCLAGroupTemplateHandler = template.CreateCLAGroupTemplateHandlerFunc(func(params template.CreateCLAGroupTemplateParams, user *auth.User) middleware.Responder { + api.TemplateCreateCLAGroupTemplateHandler = template.CreateCLAGroupTemplateHandlerFunc(func(params template.CreateCLAGroupTemplateParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(params.HTTPRequest.Context(), utils.XREQUESTID, reqID) // nolint - utils.SetAuthUserProperties(user, params.XUSERNAME, params.XEMAIL) + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "TemplateCreateCLAGroupTemplateHandler", + "functionName": "v2.template.handlers.TemplateCreateCLAGroupTemplateHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": params.ClaGroupID, } + projectCLAGroups, lookupErr := v1ProjectClaGroupService.GetProjectsIdsForClaGroup(ctx, params.ClaGroupID) + if lookupErr != nil || len(projectCLAGroups) == 0 { + msg := fmt.Sprintf("unable to lookup CLA Group mapping using CLA Group ID: %s", params.ClaGroupID) + return template.NewGetTemplatesBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, lookupErr)) + } + projectSFIDs := getProjectSFIDList(projectCLAGroups) + + // Check authorization + if !utils.IsUserAuthorizedForAnyProjects(ctx, authUser, projectSFIDs, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("authUser '%s' does not have access to create CLA Group template with Project scope of any %s", + authUser.UserName, strings.Join(projectSFIDs, ",")) + log.WithFields(f).Debug(msg) + return template.NewGetTemplatesForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) + } + input := &v1Models.CreateClaGroupTemplate{} err := copier.Copy(input, ¶ms.Body) if err != nil { log.WithFields(f).WithError(err).Warn("problem converting templates") return template.NewGetTemplatesInternalServerError().WithPayload(errorResponse(reqID, err)) } - pdfUrls, err := service.CreateCLAGroupTemplate(params.HTTPRequest.Context(), params.ClaGroupID, input) + + pdfUrls, err := service.CreateCLAGroupTemplate(ctx, params.ClaGroupID, input) if err != nil { log.WithFields(f).WithError(err).Warnf("Error generating PDFs from provided templates, error: %v", err) return template.NewGetTemplatesBadRequest().WithPayload(errorResponse(reqID, err)) } + // Need the template name for the event log + templateName, lookupErr := service.GetTemplateName(ctx, input.TemplateID) + if lookupErr != nil || templateName == "" { + log.WithFields(f).WithError(lookupErr).Warnf("Error looking up template name with ID: %s", input.TemplateID) + return template.NewGetTemplatesBadRequest().WithPayload(errorResponse(reqID, err)) + } + + // Grab the new POC value from the request + newPOCValue := "" + for _, field := range input.MetaFields { + if field.TemplateVariable == "CONTACT_EMAIL" { + newPOCValue = field.Value + break + } + } + eventsService.LogEvent(&events.LogEventArgs{ - EventType: events.CLATemplateCreated, - ProjectID: params.ClaGroupID, - LfUsername: user.UserName, - EventData: &events.CLATemplateCreatedEventData{}, + EventType: events.CLATemplateCreated, + ProjectID: params.ClaGroupID, + ProjectSFID: projectCLAGroups[0].ProjectSFID, + ParentProjectSFID: projectCLAGroups[0].FoundationSFID, + LfUsername: authUser.UserName, + EventData: &events.CLATemplateCreatedEventData{ + TemplateName: templateName, + NewPOC: newPOCValue, + }, }) response := &models.TemplatePdfs{} @@ -94,7 +135,7 @@ func Configure(api *operations.EasyclaAPI, service v1Template.Service, eventsSer ctx := context.WithValue(params.HTTPRequest.Context(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(user, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "TemplateTemplatePreviewHandler", + "functionName": "v2.template.handlers.TemplateTemplatePreviewHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "templateFor": params.TemplateFor, } @@ -123,10 +164,10 @@ func Configure(api *operations.EasyclaAPI, service v1Template.Service, eventsSer reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(params.HTTPRequest.Context(), utils.XREQUESTID, reqID) // nolint f := logrus.Fields{ - "functionName": "TemplateGetCLATemplatePreviewHandler", + "functionName": "v2.template.handlers.TemplateGetCLATemplatePreviewHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), } - pdf, err := service.GetCLATemplatePreview(params.HTTPRequest.Context(), params.ClaGroupID, params.ClaType, *params.Watermark) + pdf, err := service.GetCLATemplatePreview(ctx, params.ClaGroupID, params.ClaType, *params.Watermark) if err != nil { log.WithFields(f).WithError(err).Warnf("Error getting PDFs for provided cla group ID : %s, error: %v", params.ClaGroupID, err) return writeResponse(http.StatusBadRequest, runtime.JSONMime, runtime.JSONProducer(), reqID, errorResponse(reqID, err)) @@ -142,6 +183,15 @@ func Configure(api *operations.EasyclaAPI, service v1Template.Service, eventsSer }) } +// getProjectSFIDList is a helper function to extract the project SFID values from the list of project to CLA group mapping records +func getProjectSFIDList(groups []*v1ProjectsCLAGroups.ProjectClaGroup) []string { + var response []string + for _, projectCLAGroup := range groups { + response = append(response, projectCLAGroup.ProjectSFID) + } + return response +} + type codedResponse interface { Code() string } diff --git a/cla-backend-go/v2/user-service/client.go b/cla-backend-go/v2/user-service/client.go index c96bfd973..d644a4212 100644 --- a/cla-backend-go/v2/user-service/client.go +++ b/cla-backend-go/v2/user-service/client.go @@ -8,11 +8,10 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "strings" - "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/sirupsen/logrus" log "github.com/communitybridge/easycla/cla-backend-go/logging" @@ -21,21 +20,20 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/communitybridge/easycla/cla-backend-go/token" "github.com/communitybridge/easycla/cla-backend-go/v2/user-service/client" - "github.com/communitybridge/easycla/cla-backend-go/v2/user-service/client/bulk" "github.com/communitybridge/easycla/cla-backend-go/v2/user-service/client/user" "github.com/communitybridge/easycla/cla-backend-go/v2/user-service/models" runtimeClient "github.com/go-openapi/runtime/client" "github.com/go-openapi/strfmt" ) -// errors var ( + // ErrUserNotFound is an error for users not found ErrUserNotFound = errors.New("user not found") ) // Client is client for user_service type Client struct { - cl *client.UserService + cl *client.UserServiceAPI apiKey string apiGwURL string } @@ -76,19 +74,57 @@ func (usc *Client) GetUsersByUsernames(lfUsernames []string) ([]*models.User, er return nil, err } - params := bulk.NewSearchBulkParams() - params.SearchBulk = &models.SearchBulk{ - List: lfUsernames, + url := fmt.Sprintf("https://%s/user-service/v1/bulk", usc.apiGwURL) + var requestBody = models.SearchBulk{ Type: aws.String("username"), + List: lfUsernames, } - clientAuth := runtimeClient.BearerToken(tok) - result, err := usc.cl.Bulk.SearchBulk(params, clientAuth) + + requestBodyBytes, err := json.Marshal(requestBody) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem marshalling the request body") + return nil, err + } + + request, err := http.NewRequest("POST", url, strings.NewReader(string(requestBodyBytes))) + + if err != nil { + log.WithFields(f).WithError(err).Warn("problem building new request") + return nil, err + } + + request.Header.Set("X-API-KEY", usc.apiKey) + request.Header.Set("Authorization", "Bearer "+tok) + request.Header.Set("Content-Type", "application/json") + + response, err := http.DefaultClient.Do(request) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem searching user") + return nil, err + } + + defer func() { + closeErr := response.Body.Close() + if closeErr != nil { + log.WithFields(f).WithError(closeErr).Warn("error closing body") + } + + }() + + data, err := io.ReadAll(response.Body) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem decoding the user response") + return nil, err + } + + //return as []*models.User + userList, err := getUsers(data) if err != nil { - log.WithFields(f).WithError(err).Warn("problem with the bulk search") + log.WithFields(f).WithError(err).Warn("problem processing the user response") return nil, err } - return result.Payload.Data, nil + return userList, nil } // GetUserByUsername returns user by lfUsername @@ -98,7 +134,6 @@ func (usc *Client) GetUserByUsername(lfUsername string) (*models.User, error) { "lfUsername": lfUsername, } - log.WithFields(f).Debug("querying user by username...") // use the ListUsers API endpoint (actually called FindUsers) with the lfUsername filter userModel, err := usc.ListUsersByUsername(lfUsername) if err != nil { @@ -154,7 +189,7 @@ func (usc *Client) SearchUsers(firstName string, lastName string, email string) } }() - data, err := ioutil.ReadAll(response.Body) + data, err := io.ReadAll(response.Body) if err != nil { log.WithFields(f).WithError(err).Warn("problem decoding the user response") return nil, err @@ -190,25 +225,51 @@ func (usc *Client) ListUsersByUsername(lfUsername string) (*models.User, error) log.WithFields(f).WithError(err).Warn("problem obtaining token") return nil, err } - clientAuth := runtimeClient.BearerToken(tok) - params := &user.FindUsersParams{ - Username: &lfUsername, - Context: utils.NewContext(), + url := fmt.Sprintf("https://%s/user-service/v1/users?username=%s", usc.apiGwURL, lfUsername) + request, err := http.NewRequest("GET", url, nil) + + if err != nil { + log.WithFields(f).WithError(err).Warn("problem building new request") + return nil, err + } + + request.Header.Set("X-API-KEY", usc.apiKey) + request.Header.Set("Authorization", "Bearer "+tok) + request.Header.Set("Content-Type", "application/json") + + response, err := http.DefaultClient.Do(request) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem searching user") + return nil, err + } + + defer func() { + closeErr := response.Body.Close() + if closeErr != nil { + log.WithFields(f).WithError(closeErr).Warn("error closing body") + } + }() + + data, err := io.ReadAll(response.Body) + + if err != nil { + log.WithFields(f).WithError(err).Warn("problem decoding the user response") + return nil, err } - result, err := usc.cl.User.FindUsers(params, clientAuth) + + userList, err := getUsers(data) if err != nil { - log.WithFields(f).WithError(err).Warn("problem finding user by lfUsername") + log.WithFields(f).WithError(err).Warn("problem processing the user response") return nil, err } - users := result.Payload.Data - if len(users) == 0 { + if len(userList) == 0 { log.WithFields(f).Debug("get by lfUsername returned no results") return nil, ErrUserNotFound } - return users[0], nil + return userList[0], nil } // SearchUsersByEmail returns a single user based on the email parameter @@ -223,63 +284,60 @@ func (usc *Client) SearchUsersByEmail(email string) (*models.User, error) { log.WithFields(f).WithError(err).Warn("problem obtaining token") return nil, err } - clientAuth := runtimeClient.BearerToken(tok) - params := &user.FindUsersParams{ - Email: &email, - Context: context.Background(), - } - result, err := usc.cl.User.FindUsers(params, clientAuth) + url := fmt.Sprintf("https://%s/user-service/v1/users?email=%s", usc.apiGwURL, email) + request, err := http.NewRequest("GET", url, nil) + if err != nil { - log.WithFields(f).WithError(err).Warn("problem finding user by email") + log.WithFields(f).WithError(err).Warn("problem building new request") return nil, err } - users := result.Payload.Data - if len(users) == 0 { - log.WithFields(f).Debug("get by lfUsername returned no results") - return nil, ErrUserNotFound - } - return users[0], nil -} + request.Header.Set("X-API-KEY", usc.apiKey) + request.Header.Set("Authorization", "Bearer "+tok) + request.Header.Set("Content-Type", "application/json") -func getUsers(body []byte) ([]*models.User, error) { - var users = new(models.UserList) - err := json.Unmarshal(body, &users) + response, err := http.DefaultClient.Do(request) if err != nil { + log.WithFields(f).WithError(err).Warn("problem searching user") return nil, err } - return users.Data, err -} -// SearchUserByEmail search user by email -func (usc *Client) SearchUserByEmail(email string) (*models.User, error) { - f := logrus.Fields{ - "functionName": "SearchUserByEmail", - "email": email, - } - params := &user.SearchUsersParams{ - Email: &email, - Context: context.Background(), - } - tok, err := token.GetToken() + defer func() { + closeErr := response.Body.Close() + if closeErr != nil { + log.WithFields(f).WithError(closeErr).Warn("error closing body") + } + }() + + data, err := io.ReadAll(response.Body) if err != nil { - log.WithFields(f).WithError(err).Warn("problem obtaining token") + log.WithFields(f).WithError(err).Warn("problem decoding the user response") return nil, err } - clientAuth := runtimeClient.BearerToken(tok) - result, err := usc.cl.User.SearchUsers(params, clientAuth) + + userList, err := getUsers(data) if err != nil { - log.WithFields(f).WithError(err).Warn("problem finding user by email") + log.WithFields(f).WithError(err).Warn("problem processing the user response") return nil, err } - users := result.Payload.Data - if len(users) == 0 { + if len(userList) == 0 { log.WithFields(f).Debug("get by lfUsername returned no results") return nil, ErrUserNotFound } - return users[0], nil + + return userList[0], nil + +} + +func getUsers(body []byte) ([]*models.User, error) { + var users = new(models.UserList) + err := json.Unmarshal(body, &users) + if err != nil { + return nil, err + } + return users.Data, err } // ConvertToContact converts user to contact from lead @@ -336,7 +394,7 @@ func (usc *Client) GetStaff(userSFID string) (*models.Staff, error) { return result.Payload, nil } -//GetUserEmail returns email of a user given username +// GetUserEmail returns email of a user given username func (usc *Client) GetUserEmail(username string) (string, error) { user, err := usc.GetUserByUsername(username) if err != nil { @@ -381,3 +439,30 @@ func (usc *Client) UpdateUserAccount(userSFID string, orgID string) error { log.WithFields(f).Debugf("successfully updated user: %s", result) return nil } + +// GetPrimaryEmail gets user primary email +func (usc *Client) GetPrimaryEmail(user *models.User) string { + f := logrus.Fields{ + "functionName": "GetPrimaryEmail", + } + primaryEmail := "" + for _, email := range user.Emails { + if *email.IsPrimary { + log.WithFields(f).Debugf("Found primary email : %s ", *email.EmailAddress) + primaryEmail = *email.EmailAddress + } + } + return primaryEmail +} + +// EmailsToSlice converts a user model's email addresses to a string slice +func (usc *Client) EmailsToSlice(user *models.User) []string { + var emailList []string + for _, email := range user.Emails { + if email.EmailAddress != nil { + emailList = append(emailList, *email.EmailAddress) + } + } + + return emailList +} diff --git a/cla-backend-go/version/handlers.go b/cla-backend-go/version/handlers.go index 6ca46609e..fcaf6d1c2 100644 --- a/cla-backend-go/version/handlers.go +++ b/cla-backend-go/version/handlers.go @@ -5,9 +5,9 @@ package version import ( "github.com/aws/aws-sdk-go/aws" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/version" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/version" "github.com/go-openapi/runtime/middleware" ) diff --git a/cla-backend-go/yarn.lock b/cla-backend-go/yarn.lock index e8b46aa70..071fc62e4 100644 --- a/cla-backend-go/yarn.lock +++ b/cla-backend-go/yarn.lock @@ -10,16 +10,927 @@ d "1" es5-ext "^0.10.47" +"@aws-crypto/crc32@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-3.0.0.tgz#07300eca214409c33e3ff769cd5697b57fdd38fa" + integrity sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA== + dependencies: + "@aws-crypto/util" "^3.0.0" + "@aws-sdk/types" "^3.222.0" + tslib "^1.11.1" + +"@aws-crypto/crc32c@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/crc32c/-/crc32c-3.0.0.tgz#016c92da559ef638a84a245eecb75c3e97cb664f" + integrity sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w== + dependencies: + "@aws-crypto/util" "^3.0.0" + "@aws-sdk/types" "^3.222.0" + tslib "^1.11.1" + +"@aws-crypto/ie11-detection@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz#640ae66b4ec3395cee6a8e94ebcd9f80c24cd688" + integrity sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q== + dependencies: + tslib "^1.11.1" + +"@aws-crypto/sha1-browser@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha1-browser/-/sha1-browser-3.0.0.tgz#f9083c00782b24714f528b1a1fef2174002266a3" + integrity sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw== + dependencies: + "@aws-crypto/ie11-detection" "^3.0.0" + "@aws-crypto/supports-web-crypto" "^3.0.0" + "@aws-crypto/util" "^3.0.0" + "@aws-sdk/types" "^3.222.0" + "@aws-sdk/util-locate-window" "^3.0.0" + "@aws-sdk/util-utf8-browser" "^3.0.0" + tslib "^1.11.1" + +"@aws-crypto/sha256-browser@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz#05f160138ab893f1c6ba5be57cfd108f05827766" + integrity sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ== + dependencies: + "@aws-crypto/ie11-detection" "^3.0.0" + "@aws-crypto/sha256-js" "^3.0.0" + "@aws-crypto/supports-web-crypto" "^3.0.0" + "@aws-crypto/util" "^3.0.0" + "@aws-sdk/types" "^3.222.0" + "@aws-sdk/util-locate-window" "^3.0.0" + "@aws-sdk/util-utf8-browser" "^3.0.0" + tslib "^1.11.1" + +"@aws-crypto/sha256-js@3.0.0", "@aws-crypto/sha256-js@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz#f06b84d550d25521e60d2a0e2a90139341e007c2" + integrity sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ== + dependencies: + "@aws-crypto/util" "^3.0.0" + "@aws-sdk/types" "^3.222.0" + tslib "^1.11.1" + +"@aws-crypto/supports-web-crypto@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz#5d1bf825afa8072af2717c3e455f35cda0103ec2" + integrity sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg== + dependencies: + tslib "^1.11.1" + +"@aws-crypto/util@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-3.0.0.tgz#1c7ca90c29293f0883468ad48117937f0fe5bfb0" + integrity sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w== + dependencies: + "@aws-sdk/types" "^3.222.0" + "@aws-sdk/util-utf8-browser" "^3.0.0" + tslib "^1.11.1" + +"@aws-sdk/client-api-gateway@^3.588.0": + version "3.592.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-api-gateway/-/client-api-gateway-3.592.0.tgz#90bc5ec6b3be66d7903e94aab1abc926a2c50c8f" + integrity sha512-dt91sbATd7iOxralMhljTZiCzY0CDfqAFG0JX9ll//W1WLYZXYqYipcC1T9QwUJfblZj+vG5jFD8s1npeytvnw== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sso-oidc" "3.592.0" + "@aws-sdk/client-sts" "3.592.0" + "@aws-sdk/core" "3.592.0" + "@aws-sdk/credential-provider-node" "3.592.0" + "@aws-sdk/middleware-host-header" "3.577.0" + "@aws-sdk/middleware-logger" "3.577.0" + "@aws-sdk/middleware-recursion-detection" "3.577.0" + "@aws-sdk/middleware-sdk-api-gateway" "3.580.0" + "@aws-sdk/middleware-user-agent" "3.587.0" + "@aws-sdk/region-config-resolver" "3.587.0" + "@aws-sdk/types" "3.577.0" + "@aws-sdk/util-endpoints" "3.587.0" + "@aws-sdk/util-user-agent-browser" "3.577.0" + "@aws-sdk/util-user-agent-node" "3.587.0" + "@smithy/config-resolver" "^3.0.1" + "@smithy/core" "^2.2.0" + "@smithy/fetch-http-handler" "^3.0.1" + "@smithy/hash-node" "^3.0.0" + "@smithy/invalid-dependency" "^3.0.0" + "@smithy/middleware-content-length" "^3.0.0" + "@smithy/middleware-endpoint" "^3.0.1" + "@smithy/middleware-retry" "^3.0.3" + "@smithy/middleware-serde" "^3.0.0" + "@smithy/middleware-stack" "^3.0.0" + "@smithy/node-config-provider" "^3.1.0" + "@smithy/node-http-handler" "^3.0.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/smithy-client" "^3.1.1" + "@smithy/types" "^3.0.0" + "@smithy/url-parser" "^3.0.0" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.3" + "@smithy/util-defaults-mode-node" "^3.0.3" + "@smithy/util-endpoints" "^2.0.1" + "@smithy/util-middleware" "^3.0.0" + "@smithy/util-retry" "^3.0.0" + "@smithy/util-stream" "^3.0.1" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/client-cloudformation@^3.410.0": + version "3.592.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-cloudformation/-/client-cloudformation-3.592.0.tgz#960ba2ad47a92d73fd8c8721a315a7eca50a5194" + integrity sha512-jZXAmbHDlCPxJx4LVVWQVbZDLykbDynh7SgO8QnYEObsqxgSqgxT4/czPbTgppwrqR4FKWIc8WRW942YrH/7rA== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sso-oidc" "3.592.0" + "@aws-sdk/client-sts" "3.592.0" + "@aws-sdk/core" "3.592.0" + "@aws-sdk/credential-provider-node" "3.592.0" + "@aws-sdk/middleware-host-header" "3.577.0" + "@aws-sdk/middleware-logger" "3.577.0" + "@aws-sdk/middleware-recursion-detection" "3.577.0" + "@aws-sdk/middleware-user-agent" "3.587.0" + "@aws-sdk/region-config-resolver" "3.587.0" + "@aws-sdk/types" "3.577.0" + "@aws-sdk/util-endpoints" "3.587.0" + "@aws-sdk/util-user-agent-browser" "3.577.0" + "@aws-sdk/util-user-agent-node" "3.587.0" + "@smithy/config-resolver" "^3.0.1" + "@smithy/core" "^2.2.0" + "@smithy/fetch-http-handler" "^3.0.1" + "@smithy/hash-node" "^3.0.0" + "@smithy/invalid-dependency" "^3.0.0" + "@smithy/middleware-content-length" "^3.0.0" + "@smithy/middleware-endpoint" "^3.0.1" + "@smithy/middleware-retry" "^3.0.3" + "@smithy/middleware-serde" "^3.0.0" + "@smithy/middleware-stack" "^3.0.0" + "@smithy/node-config-provider" "^3.1.0" + "@smithy/node-http-handler" "^3.0.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/smithy-client" "^3.1.1" + "@smithy/types" "^3.0.0" + "@smithy/url-parser" "^3.0.0" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.3" + "@smithy/util-defaults-mode-node" "^3.0.3" + "@smithy/util-endpoints" "^2.0.1" + "@smithy/util-middleware" "^3.0.0" + "@smithy/util-retry" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + "@smithy/util-waiter" "^3.0.0" + tslib "^2.6.2" + uuid "^9.0.1" + +"@aws-sdk/client-cognito-identity-provider@^3.588.0": + version "3.592.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-cognito-identity-provider/-/client-cognito-identity-provider-3.592.0.tgz#c3ebef3a140b6184828345e4b63c192a227db6f2" + integrity sha512-2DiNGEHYnlKCMzb4KBPr+mYqvHsPLUjJ67/vp6e6iB1emWXi/VAuiqx9Jom7t86TM9XZCUcm3s9rHoykU0cDAw== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sso-oidc" "3.592.0" + "@aws-sdk/client-sts" "3.592.0" + "@aws-sdk/core" "3.592.0" + "@aws-sdk/credential-provider-node" "3.592.0" + "@aws-sdk/middleware-host-header" "3.577.0" + "@aws-sdk/middleware-logger" "3.577.0" + "@aws-sdk/middleware-recursion-detection" "3.577.0" + "@aws-sdk/middleware-user-agent" "3.587.0" + "@aws-sdk/region-config-resolver" "3.587.0" + "@aws-sdk/types" "3.577.0" + "@aws-sdk/util-endpoints" "3.587.0" + "@aws-sdk/util-user-agent-browser" "3.577.0" + "@aws-sdk/util-user-agent-node" "3.587.0" + "@smithy/config-resolver" "^3.0.1" + "@smithy/core" "^2.2.0" + "@smithy/fetch-http-handler" "^3.0.1" + "@smithy/hash-node" "^3.0.0" + "@smithy/invalid-dependency" "^3.0.0" + "@smithy/middleware-content-length" "^3.0.0" + "@smithy/middleware-endpoint" "^3.0.1" + "@smithy/middleware-retry" "^3.0.3" + "@smithy/middleware-serde" "^3.0.0" + "@smithy/middleware-stack" "^3.0.0" + "@smithy/node-config-provider" "^3.1.0" + "@smithy/node-http-handler" "^3.0.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/smithy-client" "^3.1.1" + "@smithy/types" "^3.0.0" + "@smithy/url-parser" "^3.0.0" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.3" + "@smithy/util-defaults-mode-node" "^3.0.3" + "@smithy/util-endpoints" "^2.0.1" + "@smithy/util-middleware" "^3.0.0" + "@smithy/util-retry" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/client-eventbridge@^3.588.0": + version "3.592.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-eventbridge/-/client-eventbridge-3.592.0.tgz#2a83aadbaa575ff8e330b45bfd961e900c6841dc" + integrity sha512-wjAuC8YWm07y8ItAqFqndnnjN8COpAi226Dt+8wNzooGqaMU6F46xNLIFuezbs8hOK5kxrpY0nNYUcD4TzZK9Q== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sso-oidc" "3.592.0" + "@aws-sdk/client-sts" "3.592.0" + "@aws-sdk/core" "3.592.0" + "@aws-sdk/credential-provider-node" "3.592.0" + "@aws-sdk/middleware-host-header" "3.577.0" + "@aws-sdk/middleware-logger" "3.577.0" + "@aws-sdk/middleware-recursion-detection" "3.577.0" + "@aws-sdk/middleware-signing" "3.587.0" + "@aws-sdk/middleware-user-agent" "3.587.0" + "@aws-sdk/region-config-resolver" "3.587.0" + "@aws-sdk/signature-v4-multi-region" "3.587.0" + "@aws-sdk/types" "3.577.0" + "@aws-sdk/util-endpoints" "3.587.0" + "@aws-sdk/util-user-agent-browser" "3.577.0" + "@aws-sdk/util-user-agent-node" "3.587.0" + "@smithy/config-resolver" "^3.0.1" + "@smithy/fetch-http-handler" "^3.0.1" + "@smithy/hash-node" "^3.0.0" + "@smithy/invalid-dependency" "^3.0.0" + "@smithy/middleware-content-length" "^3.0.0" + "@smithy/middleware-endpoint" "^3.0.1" + "@smithy/middleware-retry" "^3.0.3" + "@smithy/middleware-serde" "^3.0.0" + "@smithy/middleware-stack" "^3.0.0" + "@smithy/node-config-provider" "^3.1.0" + "@smithy/node-http-handler" "^3.0.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/smithy-client" "^3.1.1" + "@smithy/types" "^3.0.0" + "@smithy/url-parser" "^3.0.0" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.3" + "@smithy/util-defaults-mode-node" "^3.0.3" + "@smithy/util-endpoints" "^2.0.1" + "@smithy/util-retry" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/client-iam@^3.588.0": + version "3.592.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-iam/-/client-iam-3.592.0.tgz#0b4f1e8347a7a1497f0ac74417f3fe8329f5fd3a" + integrity sha512-ufJDnT51cJrT4NI1wSpqq4+/dSYprw6g3qYxLe8Hl30O08lkFNeQTtO1jUdkHBohtMlwlTNrGxq+SUxV5cHw4w== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sso-oidc" "3.592.0" + "@aws-sdk/client-sts" "3.592.0" + "@aws-sdk/core" "3.592.0" + "@aws-sdk/credential-provider-node" "3.592.0" + "@aws-sdk/middleware-host-header" "3.577.0" + "@aws-sdk/middleware-logger" "3.577.0" + "@aws-sdk/middleware-recursion-detection" "3.577.0" + "@aws-sdk/middleware-user-agent" "3.587.0" + "@aws-sdk/region-config-resolver" "3.587.0" + "@aws-sdk/types" "3.577.0" + "@aws-sdk/util-endpoints" "3.587.0" + "@aws-sdk/util-user-agent-browser" "3.577.0" + "@aws-sdk/util-user-agent-node" "3.587.0" + "@smithy/config-resolver" "^3.0.1" + "@smithy/core" "^2.2.0" + "@smithy/fetch-http-handler" "^3.0.1" + "@smithy/hash-node" "^3.0.0" + "@smithy/invalid-dependency" "^3.0.0" + "@smithy/middleware-content-length" "^3.0.0" + "@smithy/middleware-endpoint" "^3.0.1" + "@smithy/middleware-retry" "^3.0.3" + "@smithy/middleware-serde" "^3.0.0" + "@smithy/middleware-stack" "^3.0.0" + "@smithy/node-config-provider" "^3.1.0" + "@smithy/node-http-handler" "^3.0.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/smithy-client" "^3.1.1" + "@smithy/types" "^3.0.0" + "@smithy/url-parser" "^3.0.0" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.3" + "@smithy/util-defaults-mode-node" "^3.0.3" + "@smithy/util-endpoints" "^2.0.1" + "@smithy/util-middleware" "^3.0.0" + "@smithy/util-retry" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + "@smithy/util-waiter" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/client-lambda@^3.588.0": + version "3.592.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-lambda/-/client-lambda-3.592.0.tgz#35b512d39f1066e4c6c10db63d778ed3e44f2109" + integrity sha512-uCtyrccg+qZ/KbZtY9OHb8dXG59yYDvoQULiQaj+73XkI/P4Z69prflg87cA5UpXoSeoAinCahwyJM5+G/EXYw== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sso-oidc" "3.592.0" + "@aws-sdk/client-sts" "3.592.0" + "@aws-sdk/core" "3.592.0" + "@aws-sdk/credential-provider-node" "3.592.0" + "@aws-sdk/middleware-host-header" "3.577.0" + "@aws-sdk/middleware-logger" "3.577.0" + "@aws-sdk/middleware-recursion-detection" "3.577.0" + "@aws-sdk/middleware-user-agent" "3.587.0" + "@aws-sdk/region-config-resolver" "3.587.0" + "@aws-sdk/types" "3.577.0" + "@aws-sdk/util-endpoints" "3.587.0" + "@aws-sdk/util-user-agent-browser" "3.577.0" + "@aws-sdk/util-user-agent-node" "3.587.0" + "@smithy/config-resolver" "^3.0.1" + "@smithy/core" "^2.2.0" + "@smithy/eventstream-serde-browser" "^3.0.0" + "@smithy/eventstream-serde-config-resolver" "^3.0.0" + "@smithy/eventstream-serde-node" "^3.0.0" + "@smithy/fetch-http-handler" "^3.0.1" + "@smithy/hash-node" "^3.0.0" + "@smithy/invalid-dependency" "^3.0.0" + "@smithy/middleware-content-length" "^3.0.0" + "@smithy/middleware-endpoint" "^3.0.1" + "@smithy/middleware-retry" "^3.0.3" + "@smithy/middleware-serde" "^3.0.0" + "@smithy/middleware-stack" "^3.0.0" + "@smithy/node-config-provider" "^3.1.0" + "@smithy/node-http-handler" "^3.0.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/smithy-client" "^3.1.1" + "@smithy/types" "^3.0.0" + "@smithy/url-parser" "^3.0.0" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.3" + "@smithy/util-defaults-mode-node" "^3.0.3" + "@smithy/util-endpoints" "^2.0.1" + "@smithy/util-middleware" "^3.0.0" + "@smithy/util-retry" "^3.0.0" + "@smithy/util-stream" "^3.0.1" + "@smithy/util-utf8" "^3.0.0" + "@smithy/util-waiter" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/client-s3@^3.588.0": + version "3.592.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.592.0.tgz#ed9cfb1e968ecad06b716ffc20c02687ca789801" + integrity sha512-abn1XYk9HW2nXIvyD6ldwrNcF5/7a2p06OSWEr7zVTo954kArg8N0yTsy83ezznEHZfaZpdZn/DLDl2GxrE1Xw== + dependencies: + "@aws-crypto/sha1-browser" "3.0.0" + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sso-oidc" "3.592.0" + "@aws-sdk/client-sts" "3.592.0" + "@aws-sdk/core" "3.592.0" + "@aws-sdk/credential-provider-node" "3.592.0" + "@aws-sdk/middleware-bucket-endpoint" "3.587.0" + "@aws-sdk/middleware-expect-continue" "3.577.0" + "@aws-sdk/middleware-flexible-checksums" "3.587.0" + "@aws-sdk/middleware-host-header" "3.577.0" + "@aws-sdk/middleware-location-constraint" "3.577.0" + "@aws-sdk/middleware-logger" "3.577.0" + "@aws-sdk/middleware-recursion-detection" "3.577.0" + "@aws-sdk/middleware-sdk-s3" "3.587.0" + "@aws-sdk/middleware-signing" "3.587.0" + "@aws-sdk/middleware-ssec" "3.577.0" + "@aws-sdk/middleware-user-agent" "3.587.0" + "@aws-sdk/region-config-resolver" "3.587.0" + "@aws-sdk/signature-v4-multi-region" "3.587.0" + "@aws-sdk/types" "3.577.0" + "@aws-sdk/util-endpoints" "3.587.0" + "@aws-sdk/util-user-agent-browser" "3.577.0" + "@aws-sdk/util-user-agent-node" "3.587.0" + "@aws-sdk/xml-builder" "3.575.0" + "@smithy/config-resolver" "^3.0.1" + "@smithy/core" "^2.2.0" + "@smithy/eventstream-serde-browser" "^3.0.0" + "@smithy/eventstream-serde-config-resolver" "^3.0.0" + "@smithy/eventstream-serde-node" "^3.0.0" + "@smithy/fetch-http-handler" "^3.0.1" + "@smithy/hash-blob-browser" "^3.0.0" + "@smithy/hash-node" "^3.0.0" + "@smithy/hash-stream-node" "^3.0.0" + "@smithy/invalid-dependency" "^3.0.0" + "@smithy/md5-js" "^3.0.0" + "@smithy/middleware-content-length" "^3.0.0" + "@smithy/middleware-endpoint" "^3.0.1" + "@smithy/middleware-retry" "^3.0.3" + "@smithy/middleware-serde" "^3.0.0" + "@smithy/middleware-stack" "^3.0.0" + "@smithy/node-config-provider" "^3.1.0" + "@smithy/node-http-handler" "^3.0.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/smithy-client" "^3.1.1" + "@smithy/types" "^3.0.0" + "@smithy/url-parser" "^3.0.0" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.3" + "@smithy/util-defaults-mode-node" "^3.0.3" + "@smithy/util-endpoints" "^2.0.1" + "@smithy/util-retry" "^3.0.0" + "@smithy/util-stream" "^3.0.1" + "@smithy/util-utf8" "^3.0.0" + "@smithy/util-waiter" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/client-sso-oidc@3.592.0": + version "3.592.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.592.0.tgz#0e5826e17a3d4db52cd38d0146e6faf520812cfe" + integrity sha512-11Zvm8nm0s/UF3XCjzFRpQU+8FFVW5rcr3BHfnH6xAe5JEoN6bJN/n+wOfnElnjek+90hh+Qc7s141AMrCjiiw== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sts" "3.592.0" + "@aws-sdk/core" "3.592.0" + "@aws-sdk/credential-provider-node" "3.592.0" + "@aws-sdk/middleware-host-header" "3.577.0" + "@aws-sdk/middleware-logger" "3.577.0" + "@aws-sdk/middleware-recursion-detection" "3.577.0" + "@aws-sdk/middleware-user-agent" "3.587.0" + "@aws-sdk/region-config-resolver" "3.587.0" + "@aws-sdk/types" "3.577.0" + "@aws-sdk/util-endpoints" "3.587.0" + "@aws-sdk/util-user-agent-browser" "3.577.0" + "@aws-sdk/util-user-agent-node" "3.587.0" + "@smithy/config-resolver" "^3.0.1" + "@smithy/core" "^2.2.0" + "@smithy/fetch-http-handler" "^3.0.1" + "@smithy/hash-node" "^3.0.0" + "@smithy/invalid-dependency" "^3.0.0" + "@smithy/middleware-content-length" "^3.0.0" + "@smithy/middleware-endpoint" "^3.0.1" + "@smithy/middleware-retry" "^3.0.3" + "@smithy/middleware-serde" "^3.0.0" + "@smithy/middleware-stack" "^3.0.0" + "@smithy/node-config-provider" "^3.1.0" + "@smithy/node-http-handler" "^3.0.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/smithy-client" "^3.1.1" + "@smithy/types" "^3.0.0" + "@smithy/url-parser" "^3.0.0" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.3" + "@smithy/util-defaults-mode-node" "^3.0.3" + "@smithy/util-endpoints" "^2.0.1" + "@smithy/util-middleware" "^3.0.0" + "@smithy/util-retry" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/client-sso@3.592.0": + version "3.592.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.592.0.tgz#90462e744998990079c28a083553090af9ac2902" + integrity sha512-w+SuW47jQqvOC7fonyjFjsOh3yjqJ+VpWdVrmrl0E/KryBE7ho/Wn991Buf/EiHHeJikoWgHsAIPkBH29+ntdA== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/core" "3.592.0" + "@aws-sdk/middleware-host-header" "3.577.0" + "@aws-sdk/middleware-logger" "3.577.0" + "@aws-sdk/middleware-recursion-detection" "3.577.0" + "@aws-sdk/middleware-user-agent" "3.587.0" + "@aws-sdk/region-config-resolver" "3.587.0" + "@aws-sdk/types" "3.577.0" + "@aws-sdk/util-endpoints" "3.587.0" + "@aws-sdk/util-user-agent-browser" "3.577.0" + "@aws-sdk/util-user-agent-node" "3.587.0" + "@smithy/config-resolver" "^3.0.1" + "@smithy/core" "^2.2.0" + "@smithy/fetch-http-handler" "^3.0.1" + "@smithy/hash-node" "^3.0.0" + "@smithy/invalid-dependency" "^3.0.0" + "@smithy/middleware-content-length" "^3.0.0" + "@smithy/middleware-endpoint" "^3.0.1" + "@smithy/middleware-retry" "^3.0.3" + "@smithy/middleware-serde" "^3.0.0" + "@smithy/middleware-stack" "^3.0.0" + "@smithy/node-config-provider" "^3.1.0" + "@smithy/node-http-handler" "^3.0.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/smithy-client" "^3.1.1" + "@smithy/types" "^3.0.0" + "@smithy/url-parser" "^3.0.0" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.3" + "@smithy/util-defaults-mode-node" "^3.0.3" + "@smithy/util-endpoints" "^2.0.1" + "@smithy/util-middleware" "^3.0.0" + "@smithy/util-retry" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/client-sts@3.592.0", "@aws-sdk/client-sts@^3.410.0": + version "3.592.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.592.0.tgz#8a24080785355ced48ed5b49ab23d1eaf9f70f47" + integrity sha512-KUrOdszZfcrlpKr4dpdkGibZ/qq3Lnfu1rjv1U+V1QJQ9OuMo9J3sDWpWV9tigNqY0aGllarWH5cJbz9868W/w== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sso-oidc" "3.592.0" + "@aws-sdk/core" "3.592.0" + "@aws-sdk/credential-provider-node" "3.592.0" + "@aws-sdk/middleware-host-header" "3.577.0" + "@aws-sdk/middleware-logger" "3.577.0" + "@aws-sdk/middleware-recursion-detection" "3.577.0" + "@aws-sdk/middleware-user-agent" "3.587.0" + "@aws-sdk/region-config-resolver" "3.587.0" + "@aws-sdk/types" "3.577.0" + "@aws-sdk/util-endpoints" "3.587.0" + "@aws-sdk/util-user-agent-browser" "3.577.0" + "@aws-sdk/util-user-agent-node" "3.587.0" + "@smithy/config-resolver" "^3.0.1" + "@smithy/core" "^2.2.0" + "@smithy/fetch-http-handler" "^3.0.1" + "@smithy/hash-node" "^3.0.0" + "@smithy/invalid-dependency" "^3.0.0" + "@smithy/middleware-content-length" "^3.0.0" + "@smithy/middleware-endpoint" "^3.0.1" + "@smithy/middleware-retry" "^3.0.3" + "@smithy/middleware-serde" "^3.0.0" + "@smithy/middleware-stack" "^3.0.0" + "@smithy/node-config-provider" "^3.1.0" + "@smithy/node-http-handler" "^3.0.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/smithy-client" "^3.1.1" + "@smithy/types" "^3.0.0" + "@smithy/url-parser" "^3.0.0" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.3" + "@smithy/util-defaults-mode-node" "^3.0.3" + "@smithy/util-endpoints" "^2.0.1" + "@smithy/util-middleware" "^3.0.0" + "@smithy/util-retry" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/core@3.592.0": + version "3.592.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.592.0.tgz#d903a3993f8ba6836480314c2a8af8b7857bb943" + integrity sha512-gLPMXR/HXDP+9gXAt58t7gaMTvRts9i6Q7NMISpkGF54wehskl5WGrbdtHJFylrlJ5BQo3XVY6i661o+EuR1wg== + dependencies: + "@smithy/core" "^2.2.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/signature-v4" "^3.0.0" + "@smithy/smithy-client" "^3.1.1" + "@smithy/types" "^3.0.0" + fast-xml-parser "4.2.5" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-env@3.587.0": + version "3.587.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.587.0.tgz#40435be331773e4b1b665a1f4963518d4647505c" + integrity sha512-Hyg/5KFECIk2k5o8wnVEiniV86yVkhn5kzITUydmNGCkXdBFHMHRx6hleQ1bqwJHbBskyu8nbYamzcwymmGwmw== + dependencies: + "@aws-sdk/types" "3.577.0" + "@smithy/property-provider" "^3.1.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-http@3.587.0": + version "3.587.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.587.0.tgz#dc23c6d6708bc67baea54bfab0f256c5fe4df023" + integrity sha512-Su1SRWVRCuR1e32oxX3C1V4c5hpPN20WYcRfdcr2wXwHqSvys5DrnmuCC+JoEnS/zt3adUJhPliTqpfKgSdMrA== + dependencies: + "@aws-sdk/types" "3.577.0" + "@smithy/fetch-http-handler" "^3.0.1" + "@smithy/node-http-handler" "^3.0.0" + "@smithy/property-provider" "^3.1.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/smithy-client" "^3.1.1" + "@smithy/types" "^3.0.0" + "@smithy/util-stream" "^3.0.1" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-ini@3.592.0": + version "3.592.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.592.0.tgz#02b85eaca21fe54d4d285009b64a8add032a042b" + integrity sha512-3kG6ngCIOPbLJZZ3RV+NsU7HVK6vX1+1DrPJKj9fVlPYn7IXsk8NAaUT5885yC7+jKizjv0cWLrLKvAJV5gfUA== + dependencies: + "@aws-sdk/credential-provider-env" "3.587.0" + "@aws-sdk/credential-provider-http" "3.587.0" + "@aws-sdk/credential-provider-process" "3.587.0" + "@aws-sdk/credential-provider-sso" "3.592.0" + "@aws-sdk/credential-provider-web-identity" "3.587.0" + "@aws-sdk/types" "3.577.0" + "@smithy/credential-provider-imds" "^3.1.0" + "@smithy/property-provider" "^3.1.0" + "@smithy/shared-ini-file-loader" "^3.1.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-node@3.592.0": + version "3.592.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.592.0.tgz#b8339b1bfdea39b17e5da1a502b60f0fe3dde126" + integrity sha512-BguihBGTrEjVBQ07hm+ZsO29eNJaxwBwUZMftgGAm2XcMIEClNPfm5hydxu2BmA4ouIJQJ6nG8pNYghEumM+Aw== + dependencies: + "@aws-sdk/credential-provider-env" "3.587.0" + "@aws-sdk/credential-provider-http" "3.587.0" + "@aws-sdk/credential-provider-ini" "3.592.0" + "@aws-sdk/credential-provider-process" "3.587.0" + "@aws-sdk/credential-provider-sso" "3.592.0" + "@aws-sdk/credential-provider-web-identity" "3.587.0" + "@aws-sdk/types" "3.577.0" + "@smithy/credential-provider-imds" "^3.1.0" + "@smithy/property-provider" "^3.1.0" + "@smithy/shared-ini-file-loader" "^3.1.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-process@3.587.0": + version "3.587.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.587.0.tgz#1e5cc562a68438a77f464adc0493b02e04dd3ea1" + integrity sha512-V4xT3iCqkF8uL6QC4gqBJg/2asd/damswP1h9HCfqTllmPWzImS+8WD3VjgTLw5b0KbTy+ZdUhKc0wDnyzkzxg== + dependencies: + "@aws-sdk/types" "3.577.0" + "@smithy/property-provider" "^3.1.0" + "@smithy/shared-ini-file-loader" "^3.1.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-sso@3.592.0": + version "3.592.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.592.0.tgz#340649b4f5b4fbcb816f248089979d7d38ce96d3" + integrity sha512-fYFzAdDHKHvhtufPPtrLdSv8lO6GuW3em6n3erM5uFdpGytNpjXvr3XGokIsuXcNkETAY/Xihg+G9ksNE8WJxQ== + dependencies: + "@aws-sdk/client-sso" "3.592.0" + "@aws-sdk/token-providers" "3.587.0" + "@aws-sdk/types" "3.577.0" + "@smithy/property-provider" "^3.1.0" + "@smithy/shared-ini-file-loader" "^3.1.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-web-identity@3.587.0": + version "3.587.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.587.0.tgz#daa41e3cc9309594327056e431b8065145c5297a" + integrity sha512-XqIx/I2PG7kyuw3WjAP9wKlxy8IvFJwB8asOFT1xPFoVfZYKIogjG9oLP5YiRtfvDkWIztHmg5MlVv3HdJDGRw== + dependencies: + "@aws-sdk/types" "3.577.0" + "@smithy/property-provider" "^3.1.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-bucket-endpoint@3.587.0": + version "3.587.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.587.0.tgz#def5edbadf53bdfe765aa9acf12f119eb208b22f" + integrity sha512-HkFXLPl8pr6BH/Q0JpOESqEKL0ZK3sk7aSZ1S6GE4RXET7H5R94THULXqQFZzD48gZcyFooO/yNKZTqrZFaWKg== + dependencies: + "@aws-sdk/types" "3.577.0" + "@aws-sdk/util-arn-parser" "3.568.0" + "@smithy/node-config-provider" "^3.1.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/types" "^3.0.0" + "@smithy/util-config-provider" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-expect-continue@3.577.0": + version "3.577.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.577.0.tgz#47add47f17873a6044cb140f17033cb6e1d02744" + integrity sha512-6dPp8Tv4F0of4un5IAyG6q++GrRrNQQ4P2NAMB1W0VO4JoEu1C8GievbbDLi88TFIFmtKpnHB0ODCzwnoe8JsA== + dependencies: + "@aws-sdk/types" "3.577.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-flexible-checksums@3.587.0": + version "3.587.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.587.0.tgz#74afe7bd3088adf05b2ed843ad41386e793e0397" + integrity sha512-URMwp/budDvKhIvZ4a6zIBfFTun/iDlPWXqsGKYjEtHt8jz27OSjCZtDtIeqW4WTBdKL8KZgQcl+DdaE5M1qiQ== + dependencies: + "@aws-crypto/crc32" "3.0.0" + "@aws-crypto/crc32c" "3.0.0" + "@aws-sdk/types" "3.577.0" + "@smithy/is-array-buffer" "^3.0.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/types" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-host-header@3.577.0": + version "3.577.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.577.0.tgz#a3fc626d409ec850296740478c64ef5806d8b878" + integrity sha512-9ca5MJz455CODIVXs0/sWmJm7t3QO4EUa1zf8pE8grLpzf0J94bz/skDWm37Pli13T3WaAQBHCTiH2gUVfCsWg== + dependencies: + "@aws-sdk/types" "3.577.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-location-constraint@3.577.0": + version "3.577.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.577.0.tgz#9372441a4ac5747b3176ac6378d92866a51de815" + integrity sha512-DKPTD2D2s+t2QUo/IXYtVa/6Un8GZ+phSTBkyBNx2kfZz4Kwavhl/JJzSqTV3GfCXkVdFu7CrjoX7BZ6qWeTUA== + dependencies: + "@aws-sdk/types" "3.577.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-logger@3.577.0": + version "3.577.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.577.0.tgz#6da3b13ae284fb3930961f0fc8e20b1f6cf8be30" + integrity sha512-aPFGpGjTZcJYk+24bg7jT4XdIp42mFXSuPt49lw5KygefLyJM/sB0bKKqPYYivW0rcuZ9brQ58eZUNthrzYAvg== + dependencies: + "@aws-sdk/types" "3.577.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-recursion-detection@3.577.0": + version "3.577.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.577.0.tgz#fff76abc6d4521636f9e654ce5bf2c4c79249417" + integrity sha512-pn3ZVEd2iobKJlR3H+bDilHjgRnNrQ6HMmK9ZzZw89Ckn3Dcbv48xOv4RJvu0aU8SDLl/SNCxppKjeLDTPGBNA== + dependencies: + "@aws-sdk/types" "3.577.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-sdk-api-gateway@3.580.0": + version "3.580.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-api-gateway/-/middleware-sdk-api-gateway-3.580.0.tgz#ffa7587e94faec8eb36fb6f2c82fe332fcc9daf4" + integrity sha512-+6IsjfdDUK0171gQkBmVTRVMg1ZvHXNoxbhZ8MDUJbGDNsAiBJX16mj+TlOuIIrw9bnsuERunmjCBmNJ2bS/Cg== + dependencies: + "@aws-sdk/types" "3.577.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-sdk-s3@3.587.0": + version "3.587.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.587.0.tgz#720620ccdc2eb6ecab0f3a6adbd28fc27fdc70ce" + integrity sha512-vtXTGEiw1E9Fax4LmcU2Z208gbrC8ShrdsSLmGcRPpu5NPOGBFBSDG5sy5EDNClrFxIl/Le8coQnD0EDBtx+uQ== + dependencies: + "@aws-sdk/types" "3.577.0" + "@aws-sdk/util-arn-parser" "3.568.0" + "@smithy/node-config-provider" "^3.1.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/signature-v4" "^3.0.0" + "@smithy/smithy-client" "^3.1.1" + "@smithy/types" "^3.0.0" + "@smithy/util-config-provider" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-signing@3.587.0": + version "3.587.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.587.0.tgz#593c418c09c51c0bc55f23a7a6b0fda8502a8103" + integrity sha512-tiZaTDj4RvhXGRAlncFn7CSEfL3iNPO67WSaxAq+Ls5j1VgczPhu5262cWONNoMgth3nXR1hhLC4ITSl/a6AzA== + dependencies: + "@aws-sdk/types" "3.577.0" + "@smithy/property-provider" "^3.1.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/signature-v4" "^3.0.0" + "@smithy/types" "^3.0.0" + "@smithy/util-middleware" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-ssec@3.577.0": + version "3.577.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.577.0.tgz#9fcd74e8bf2c277b4349c537cbeceba279166f32" + integrity sha512-i2BPJR+rp8xmRVIGc0h1kDRFcM2J9GnClqqpc+NLSjmYadlcg4mPklisz9HzwFVcRPJ5XcGf3U4BYs5G8+iTyg== + dependencies: + "@aws-sdk/types" "3.577.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-user-agent@3.587.0": + version "3.587.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.587.0.tgz#2a68900cfb29afbae2952d901de4fcb91850bd3d" + integrity sha512-SyDomN+IOrygLucziG7/nOHkjUXES5oH5T7p8AboO8oakMQJdnudNXiYWTicQWO52R51U6CR27rcMPTGeMedYA== + dependencies: + "@aws-sdk/types" "3.577.0" + "@aws-sdk/util-endpoints" "3.587.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/region-config-resolver@3.587.0": + version "3.587.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.587.0.tgz#ad1c15494f44dfc4c7a7bce389f8b128dace923f" + integrity sha512-93I7IPZtulZQoRK+O20IJ4a1syWwYPzoO2gc3v+/GNZflZPV3QJXuVbIm0pxBsu0n/mzKGUKqSOLPIaN098HcQ== + dependencies: + "@aws-sdk/types" "3.577.0" + "@smithy/node-config-provider" "^3.1.0" + "@smithy/types" "^3.0.0" + "@smithy/util-config-provider" "^3.0.0" + "@smithy/util-middleware" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/signature-v4-multi-region@3.587.0": + version "3.587.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.587.0.tgz#f8bb6de9135f3fafab04b9220409cd0d0549b7d8" + integrity sha512-TR9+ZSjdXvXUz54ayHcCihhcvxI9W7102J1OK6MrLgBlPE7uRhAx42BR9L5lLJ86Xj3LuqPWf//o9d/zR9WVIg== + dependencies: + "@aws-sdk/middleware-sdk-s3" "3.587.0" + "@aws-sdk/types" "3.577.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/signature-v4" "^3.0.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/token-providers@3.587.0": + version "3.587.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.587.0.tgz#f9fd2ddfc554c1370f8d0f467c76a4c8cb904ae6" + integrity sha512-ULqhbnLy1hmJNRcukANBWJmum3BbjXnurLPSFXoGdV0llXYlG55SzIla2VYqdveQEEjmsBuTZdFvXAtNpmS5Zg== + dependencies: + "@aws-sdk/types" "3.577.0" + "@smithy/property-provider" "^3.1.0" + "@smithy/shared-ini-file-loader" "^3.1.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/types@3.577.0", "@aws-sdk/types@^3.222.0": + version "3.577.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.577.0.tgz#7700784d368ce386745f8c340d9d68cea4716f90" + integrity sha512-FT2JZES3wBKN/alfmhlo+3ZOq/XJ0C7QOZcDNrpKjB0kqYoKjhVKZ/Hx6ArR0czkKfHzBBEs6y40ebIHx2nSmA== + dependencies: + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/util-arn-parser@3.568.0": + version "3.568.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.568.0.tgz#6a19a8c6bbaa520b6be1c278b2b8c17875b91527" + integrity sha512-XUKJWWo+KOB7fbnPP0+g/o5Ulku/X53t7i/h+sPHr5xxYTJJ9CYnbToo95mzxe7xWvkLrsNtJ8L+MnNn9INs2w== + dependencies: + tslib "^2.6.2" + +"@aws-sdk/util-endpoints@3.587.0": + version "3.587.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.587.0.tgz#781e0822a95dba15f7ac8f22a6f6d7f0c8819818" + integrity sha512-8I1HG6Em8wQWqKcRW6m358mqebRVNpL8XrrEoT4In7xqkKkmYtHRNVYP6lcmiQh5pZ/c/FXu8dSchuFIWyEtqQ== + dependencies: + "@aws-sdk/types" "3.577.0" + "@smithy/types" "^3.0.0" + "@smithy/util-endpoints" "^2.0.1" + tslib "^2.6.2" + +"@aws-sdk/util-locate-window@^3.0.0": + version "3.568.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-locate-window/-/util-locate-window-3.568.0.tgz#2acc4b2236af0d7494f7e517401ba6b3c4af11ff" + integrity sha512-3nh4TINkXYr+H41QaPelCceEB2FXP3fxp93YZXB/kqJvX0U9j0N0Uk45gvsjmEPzG8XxkPEeLIfT2I1M7A6Lig== + dependencies: + tslib "^2.6.2" + +"@aws-sdk/util-user-agent-browser@3.577.0": + version "3.577.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.577.0.tgz#d4d2cdb3a2b3d1c8b35f239ee9f7b2c87bee66ea" + integrity sha512-zEAzHgR6HWpZOH7xFgeJLc6/CzMcx4nxeQolZxVZoB5pPaJd3CjyRhZN0xXeZB0XIRCWmb4yJBgyiugXLNMkLA== + dependencies: + "@aws-sdk/types" "3.577.0" + "@smithy/types" "^3.0.0" + bowser "^2.11.0" + tslib "^2.6.2" + +"@aws-sdk/util-user-agent-node@3.587.0": + version "3.587.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.587.0.tgz#a6bf422f307a68e16a6c19ee5d731fcc32696fb9" + integrity sha512-Pnl+DUe/bvnbEEDHP3iVJrOtE3HbFJBPgsD6vJ+ml/+IYk1Eq49jEG+EHZdNTPz3SDG0kbp2+7u41MKYJHR/iQ== + dependencies: + "@aws-sdk/types" "3.577.0" + "@smithy/node-config-provider" "^3.1.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/util-utf8-browser@^3.0.0": + version "3.259.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz#3275a6f5eb334f96ca76635b961d3c50259fd9ff" + integrity sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/xml-builder@3.575.0": + version "3.575.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.575.0.tgz#233b2aae422dd789a078073032da1bc60317aa1d" + integrity sha512-cWgAwmbFYNCFzPwxL705+lWps0F3ZvOckufd2KKoEZUmtpVw9/txUXNrPySUXSmRTSRhoatIMABNfStWR043bQ== + dependencies: + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + "@babel/runtime@^7.3.1": version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.1.tgz#b4116a6b6711d010b2dad3b7b6e43bf1b9954740" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.1.tgz" integrity sha512-J5AIf3vPj3UwXaAzb5j1xM4WAQDX3EMgemF8rjCP3SoW09LfRKAXQKt6CoVYl230P6iWdRcBbnLDDdnqWxZSCA== dependencies: regenerator-runtime "^0.13.4" "@cloudcmd/copy-file@^1.1.0": version "1.1.1" - resolved "https://registry.yarnpkg.com/@cloudcmd/copy-file/-/copy-file-1.1.1.tgz#59749cb865c7bbc748a5642b21b089704e699121" + resolved "https://registry.npmjs.org/@cloudcmd/copy-file/-/copy-file-1.1.1.tgz" integrity sha512-t6pTJdsV0qhh9YX22/Npsv95GqVABc5GRInSK7JSSNIpPLq9TM+K7odYzcOuQRPZAD9OHxZfbYsB4WJOalzqng== dependencies: es6-promisify "^6.0.0" @@ -41,7 +952,7 @@ "@nodelib/fs.scandir@2.1.3": version "2.1.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" + resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz" integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== dependencies: "@nodelib/fs.stat" "2.0.3" @@ -49,166 +960,43 @@ "@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": version "2.0.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" + resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz" integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== "@nodelib/fs.walk@^1.2.3": version "1.2.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" + resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz" integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== dependencies: "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" -"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" - integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78= - -"@protobufjs/base64@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" - integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== - -"@protobufjs/codegen@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" - integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== - -"@protobufjs/eventemitter@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" - integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A= - -"@protobufjs/fetch@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" - integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU= - dependencies: - "@protobufjs/aspromise" "^1.1.1" - "@protobufjs/inquire" "^1.1.0" - -"@protobufjs/float@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" - integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E= - -"@protobufjs/inquire@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" - integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik= - -"@protobufjs/path@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" - integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0= - -"@protobufjs/pool@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" - integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q= - -"@protobufjs/utf8@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" - integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= - -"@serverless/cli@^1.5.2": - version "1.5.2" - resolved "https://registry.yarnpkg.com/@serverless/cli/-/cli-1.5.2.tgz#7741d84ea8b5f6dcf18e21406300f01ece2865da" - integrity sha512-FMACx0qPD6Uj8U+7jDmAxEe1tdF9DsuY5VsG45nvZ3olC9xYJe/PMwxWsjXfK3tg1HUNywYAGCsy7p5fdXhNzw== - dependencies: - "@serverless/core" "^1.1.2" - "@serverless/template" "^1.1.3" - "@serverless/utils" "^1.2.0" - ansi-escapes "^4.3.1" - chalk "^2.4.2" - chokidar "^3.4.1" - dotenv "^8.2.0" - figures "^3.2.0" - minimist "^1.2.5" - prettyoutput "^1.2.0" - strip-ansi "^5.2.0" - -"@serverless/component-metrics@^1.0.8": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@serverless/component-metrics/-/component-metrics-1.0.8.tgz#a552d694863e36ee9b5095cc9cc0b5387c8dcaf9" - integrity sha512-lOUyRopNTKJYVEU9T6stp2irwlTDsYMmUKBOUjnMcwGveuUfIJqrCOtFLtIPPj3XJlbZy5F68l4KP9rZ8Ipang== - dependencies: - node-fetch "^2.6.0" - shortid "^2.2.14" - -"@serverless/components@^3.4.7": - version "3.4.7" - resolved "https://registry.yarnpkg.com/@serverless/components/-/components-3.4.7.tgz#9e5d9a58951000d9b5bcea78cad56f62d7dd5633" - integrity sha512-jY3+K3juQAa1HpFbvc1kztyDi4SFqG1+1GzUwh/kpRTlz2A01GnekWm8mf47l9HKxRzMxqVveg37wyyIQpw4xg== - dependencies: - "@serverless/platform-client" "^3.1.5" - "@serverless/platform-client-china" "^2.0.9" - "@serverless/platform-sdk" "^2.3.2" - "@serverless/utils" "^2.2.0" - adm-zip "^0.4.16" - ansi-escapes "^4.3.1" - aws4 "^1.11.0" - chalk "^4.1.0" - child-process-ext "^2.1.1" - chokidar "^3.5.0" - dotenv "^8.2.0" - figures "^3.2.0" - fs-extra "^9.0.1" - globby "^11.0.2" - got "^11.8.1" - graphlib "^2.1.8" - https-proxy-agent "^5.0.0" - ini "^1.3.8" - inquirer-autocomplete-prompt "^1.3.0" - js-yaml "^3.14.1" - memoizee "^0.4.14" - minimist "^1.2.5" - moment "^2.29.1" - open "^7.3.1" - prettyoutput "^1.2.0" - ramda "^0.27.1" - semver "^7.3.4" - strip-ansi "^6.0.0" - traverse "^0.6.6" - uuid "^8.3.2" - -"@serverless/core@^1.0.0", "@serverless/core@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@serverless/core/-/core-1.1.2.tgz#96a2ac428d81c0459474e77db6881ebdd820065d" - integrity sha512-PY7gH+7aQ+MltcUD7SRDuQODJ9Sav9HhFJsgOiyf8IVo7XVD6FxZIsSnpMI6paSkptOB7n+0Jz03gNlEkKetQQ== - dependencies: - fs-extra "^7.0.1" - js-yaml "^3.13.1" - package-json "^6.3.0" - ramda "^0.26.1" - semver "^6.1.1" - -"@serverless/enterprise-plugin@^4.4.2": - version "4.4.2" - resolved "https://registry.yarnpkg.com/@serverless/enterprise-plugin/-/enterprise-plugin-4.4.2.tgz#ec635a2099e63ecd6a82a005272cbfad8cbdfac6" - integrity sha512-w5xD8R8tFK6B7QiLvWI5jqVHTtH1LdTyGp5eRcjkdJBa10/D2IZFpJimMAGsBxk9U1JGKO4j0miVnRHIW8ppeg== +"@serverless/dashboard-plugin@^7.2.0": + version "7.2.3" + resolved "https://registry.yarnpkg.com/@serverless/dashboard-plugin/-/dashboard-plugin-7.2.3.tgz#ea2a312de2c4e763f4365654f8dfb8720bda52bb" + integrity sha512-Vu4TKJLEQ5F8ZipfCvd8A/LMIdH8kNGe448sX9mT4/Z0JVUaYmMc3BwkQ+zkNIh3QdBKAhocGn45TYjHV6uPWQ== dependencies: + "@aws-sdk/client-cloudformation" "^3.410.0" + "@aws-sdk/client-sts" "^3.410.0" "@serverless/event-mocks" "^1.1.1" - "@serverless/platform-client" "^3.1.5" - "@serverless/platform-sdk" "^2.3.2" - chalk "^4.1.0" - child-process-ext "^2.1.1" - chokidar "^3.5.0" - cli-color "^2.0.0" + "@serverless/platform-client" "^4.5.1" + "@serverless/utils" "^6.14.0" + child-process-ext "^3.0.1" + chokidar "^3.5.3" flat "^5.0.2" - fs-extra "^9.0.1" - js-yaml "^3.14.1" - jszip "^3.5.0" - lodash "^4.17.20" - memoizee "^0.4.14" - ncjsm "^4.1.0" + fs-extra "^9.1.0" + js-yaml "^4.1.0" + jszip "^3.10.1" + lodash "^4.17.21" + memoizee "^0.4.15" + ncjsm "^4.3.2" node-dir "^0.1.17" - node-fetch "^2.6.1" - open "^7.3.0" - semver "^7.3.4" - simple-git "^2.31.0" + node-fetch "^2.6.8" + open "^7.4.2" + semver "^7.3.8" + simple-git "^3.16.0" + timers-ext "^0.1.7" + type "^2.7.2" uuid "^8.3.2" yamljs "^0.3.0" @@ -220,502 +1008,719 @@ "@types/lodash" "^4.14.123" lodash "^4.17.11" -"@serverless/platform-client-china@^2.0.9": - version "2.0.9" - resolved "https://registry.yarnpkg.com/@serverless/platform-client-china/-/platform-client-china-2.0.9.tgz#473b9413781bec62c61c57b9d6ce00eb691f6f7d" - integrity sha512-qec3a5lVaMH0nccgjVgvcEF8M+M95BXZbbYDGypVHEieJQxrKqj057+VVKsiHBeHYXzr4B3v6pIyQHst40vpIw== +"@serverless/platform-client@^4.5.1": + version "4.5.1" + resolved "https://registry.yarnpkg.com/@serverless/platform-client/-/platform-client-4.5.1.tgz#db5915bb53339761e704cc3f7d352c7754a79af2" + integrity sha512-XltmO/029X76zi0LUFmhsnanhE2wnqH1xf+WBt5K8gumQA9LnrfwLgPxj+VA+mm6wQhy+PCp7H5SS0ZPu7F2Cw== dependencies: - "@serverless/utils-china" "^1.0.11" - archiver "^5.0.2" - dotenv "^8.2.0" - fs-extra "^9.0.1" - https-proxy-agent "^5.0.0" - js-yaml "^3.14.0" - minimatch "^3.0.4" - querystring "^0.2.0" - traverse "^0.6.6" - urlencode "^1.1.0" - ws "^7.3.1" - -"@serverless/platform-client@^3.1.5": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@serverless/platform-client/-/platform-client-3.4.0.tgz#8c6c94bcbf8e22a06c07b1009c500aef238024d7" - integrity sha512-iOMsluUqf7rQDalDwTRA+fuAHxk8WXCPXnMFDuTf/34q/1uRCx/xJhBNIvEUIbzZnSjiykfTIXUAcJ6kKbh6qA== - dependencies: - adm-zip "^0.4.13" - archiver "^5.0.0" - axios "^0.21.1" - fast-glob "^3.2.4" + adm-zip "^0.5.5" + archiver "^5.3.0" + axios "^1.6.2" + fast-glob "^3.2.7" https-proxy-agent "^5.0.0" ignore "^5.1.8" isomorphic-ws "^4.0.1" - js-yaml "^3.13.1" + js-yaml "^3.14.1" jwt-decode "^2.2.0" minimatch "^3.0.4" - querystring "^0.2.0" - run-parallel-limit "^1.0.6" + querystring "^0.2.1" + run-parallel-limit "^1.1.0" throat "^5.0.0" traverse "^0.6.6" - ws "^7.2.1" + ws "^7.5.3" -"@serverless/platform-sdk@^2.3.2": - version "2.3.2" - resolved "https://registry.yarnpkg.com/@serverless/platform-sdk/-/platform-sdk-2.3.2.tgz#d53e37c910e66687e0cc398c3b83fde9d7357806" - integrity sha512-JSX0/EphGVvnb4RAgZYewtBXPuVsU2TFCuXh6EEZ4jxK3WgUwNYeYdwB8EuVLrm1/dYqu/UWUC0rPKb+ZDycJg== +"@serverless/utils@^6.0.2": + version "6.11.1" + resolved "https://registry.npmjs.org/@serverless/utils/-/utils-6.11.1.tgz" + integrity sha512-HIPGwxUOtmJWTsXamJ9P3IYmvpI548c6moY+n4672a6HHo6xK2sShrQVtlJUkosMqvki30LDceydsTtHruVX3w== dependencies: - chalk "^2.4.2" - https-proxy-agent "^4.0.0" - is-docker "^1.1.0" - jwt-decode "^2.2.0" - node-fetch "^2.6.1" - opn "^5.5.0" - querystring "^0.2.0" - ramda "^0.25.0" - rc "^1.2.8" - regenerator-runtime "^0.13.7" - source-map-support "^0.5.19" - uuid "^3.4.0" - write-file-atomic "^2.4.3" - ws "<7.0.0" - -"@serverless/template@^1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@serverless/template/-/template-1.1.3.tgz#7b9e3736cc1124f176c4823fa08977cae62ae971" - integrity sha512-hcMiX523rkp6kHeKnM1x6/dXEY+d1UFSr901yVKeeCgpFy4u33UI9vlKaPweAZCF6Ahzqywf01IsFTuBVadCrQ== + archive-type "^4.0.0" + chalk "^4.1.2" + ci-info "^3.8.0" + cli-progress-footer "^2.3.2" + content-disposition "^0.5.4" + d "^1.0.1" + decompress "^4.2.1" + event-emitter "^0.3.5" + ext "^1.7.0" + ext-name "^5.0.0" + file-type "^16.5.4" + filenamify "^4.3.0" + get-stream "^6.0.1" + got "^11.8.6" + inquirer "^8.2.5" + js-yaml "^4.1.0" + jwt-decode "^3.1.2" + lodash "^4.17.21" + log "^6.3.1" + log-node "^8.0.3" + make-dir "^3.1.0" + memoizee "^0.4.15" + ms "^2.1.3" + ncjsm "^4.3.2" + node-fetch "^2.6.9" + open "^8.4.2" + p-event "^4.2.0" + supports-color "^8.1.1" + timers-ext "^0.1.7" + type "^2.7.2" + uni-global "^1.0.0" + uuid "^8.3.2" + write-file-atomic "^4.0.2" + +"@serverless/utils@^6.13.1", "@serverless/utils@^6.14.0": + version "6.15.0" + resolved "https://registry.yarnpkg.com/@serverless/utils/-/utils-6.15.0.tgz#499255c517581b1edd8c2bfedbcf61cc7aaa7539" + integrity sha512-7eDbqKv/OBd11jjdZjUwFGN8sHWkeUqLeHXHQxQ1azja2IM7WIH+z/aLgzR6LhB3/MINNwtjesDpjGqTMj2JKQ== dependencies: - "@serverless/component-metrics" "^1.0.8" - "@serverless/core" "^1.0.0" - graphlib "^2.1.7" - traverse "^0.6.6" + archive-type "^4.0.0" + chalk "^4.1.2" + ci-info "^3.8.0" + cli-progress-footer "^2.3.2" + content-disposition "^0.5.4" + d "^1.0.1" + decompress "^4.2.1" + event-emitter "^0.3.5" + ext "^1.7.0" + ext-name "^5.0.0" + file-type "^16.5.4" + filenamify "^4.3.0" + get-stream "^6.0.1" + got "^11.8.6" + inquirer "^8.2.5" + js-yaml "^4.1.0" + jwt-decode "^3.1.2" + lodash "^4.17.21" + log "^6.3.1" + log-node "^8.0.3" + make-dir "^4.0.0" + memoizee "^0.4.15" + ms "^2.1.3" + ncjsm "^4.3.2" + node-fetch "^2.6.11" + open "^8.4.2" + p-event "^4.2.0" + supports-color "^8.1.1" + timers-ext "^0.1.7" + type "^2.7.2" + uni-global "^1.0.0" + uuid "^8.3.2" + write-file-atomic "^4.0.2" -"@serverless/utils-china@^1.0.11": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@serverless/utils-china/-/utils-china-1.0.11.tgz#368003260ccd1df55f7477da50d0b606f157e58b" - integrity sha512-raOPIoPSTrkWKBDuozkYWvLXP2W65K9Uk4ud+lPcbhhBSamO3uVW40nuAkC19MdIoAsFi5oTGYpcc9UDx8b+lg== - dependencies: - "@tencent-sdk/capi" "^1.1.2" - dijkstrajs "^1.0.1" - dot-qs "0.2.0" - duplexify "^4.1.1" - end-of-stream "^1.4.4" - https-proxy-agent "^5.0.0" - kafka-node "^5.0.0" - protobufjs "^6.9.0" - qrcode-terminal "^0.12.0" - socket.io-client "^2.3.0" - winston "3.2.1" +"@sindresorhus/is@^4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.0.tgz" + integrity sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ== -"@serverless/utils@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@serverless/utils/-/utils-1.2.0.tgz#d32f2be6e9db84419c1da4b8e0e8b3706e1c69a7" - integrity sha512-aI/cpGVUhWbJUR8QDMtPue28EU4ViG/L4/XKuZDfAN2uNQv3NRjwEFIBi/cxyfQnMTYVtMLe9wDjuwzOT4ENzA== +"@smithy/abort-controller@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/abort-controller/-/abort-controller-3.0.0.tgz#5815f5d4618e14bf8d031bb98a99adabbb831168" + integrity sha512-p6GlFGBt9K4MYLu72YuJ523NVR4A8oHlC5M2JO6OmQqN8kAc/uh1JqLE+FizTokrSJGg0CSvC+BrsmGzKtsZKA== dependencies: - chalk "^2.0.1" - lodash "^4.17.15" - rc "^1.2.8" - type "^2.0.0" - uuid "^3.4.0" - write-file-atomic "^2.4.3" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -"@serverless/utils@^2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@serverless/utils/-/utils-2.2.0.tgz#80dba2a98307f9987e8c8e399381a9302dd4a39f" - integrity sha512-0TqmLwH9r2GAewvz9mhZ+TSyQBoE9ANuB4nNhn6lJvVUgzlzji3aqeFbAuDt+Z60ZkaIDNipU/J5Vf2Lo/QTQQ== +"@smithy/chunked-blob-reader-native@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-3.0.0.tgz#f1104b30030f76f9aadcbd3cdca4377bd1ba2695" + integrity sha512-VDkpCYW+peSuM4zJip5WDfqvg2Mo/e8yxOv3VF1m11y7B8KKMKVFtmZWDe36Fvk8rGuWrPZHHXZ7rR7uM5yWyg== dependencies: - chalk "^4.1.0" - inquirer "^7.3.3" - js-yaml "^4.0.0" - lodash "^4.17.20" - ncjsm "^4.1.0" - rc "^1.2.8" - type "^2.1.0" - uuid "^8.3.2" - write-file-atomic "^3.0.3" + "@smithy/util-base64" "^3.0.0" + tslib "^2.6.2" -"@sindresorhus/is@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" - integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== +"@smithy/chunked-blob-reader@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/chunked-blob-reader/-/chunked-blob-reader-3.0.0.tgz#e5d3b04e9b273ba8b7ede47461e2aa96c8aa49e0" + integrity sha512-sbnURCwjF0gSToGlsBiAmd1lRCmSn72nu9axfJu5lIx6RUEgHu6GwTMbqCdhQSi0Pumcm5vFxsi9XWXb2mTaoA== + dependencies: + tslib "^2.6.2" -"@sindresorhus/is@^0.7.0": - version "0.7.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" - integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== +"@smithy/config-resolver@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-3.0.1.tgz#4e0917e5a02139ef978a1ed470543ab41dd3626b" + integrity sha512-hbkYJc20SBDz2qqLzttjI/EqXemtmWk0ooRznLsiXp3066KQRTvuKHa7U4jCZCJq6Dozqvy0R1/vNESC9inPJg== + dependencies: + "@smithy/node-config-provider" "^3.1.0" + "@smithy/types" "^3.0.0" + "@smithy/util-config-provider" "^3.0.0" + "@smithy/util-middleware" "^3.0.0" + tslib "^2.6.2" -"@sindresorhus/is@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.0.tgz#2ff674e9611b45b528896d820d3d7a812de2f0e4" - integrity sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ== +"@smithy/core@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@smithy/core/-/core-2.2.0.tgz#f1b0837b7afa5507a9693c1e93da6ca9808022c1" + integrity sha512-ygLZSSKgt9bR8HAxR9mK+U5obvAJBr6zlQuhN5soYWx/amjDoQN4dTkydTypgKe6rIbUjTILyLU+W5XFwXr4kg== + dependencies: + "@smithy/middleware-endpoint" "^3.0.1" + "@smithy/middleware-retry" "^3.0.3" + "@smithy/middleware-serde" "^3.0.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/smithy-client" "^3.1.1" + "@smithy/types" "^3.0.0" + "@smithy/util-middleware" "^3.0.0" + tslib "^2.6.2" + +"@smithy/credential-provider-imds@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-3.1.0.tgz#7e58b78aa8de13dd04e94829241cd1cbde59b6d3" + integrity sha512-q4A4d38v8pYYmseu/jTS3Z5I3zXlEOe5Obi+EJreVKgSVyWUHOd7/yaVCinC60QG4MRyCs98tcxBH1IMC0bu7Q== + dependencies: + "@smithy/node-config-provider" "^3.1.0" + "@smithy/property-provider" "^3.1.0" + "@smithy/types" "^3.0.0" + "@smithy/url-parser" "^3.0.0" + tslib "^2.6.2" -"@szmarczak/http-timer@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" - integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== +"@smithy/eventstream-codec@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-codec/-/eventstream-codec-3.0.0.tgz#81d30391220f73d41f432f65384b606d67673e46" + integrity sha512-PUtyEA0Oik50SaEFCZ0WPVtF9tz/teze2fDptW6WRXl+RrEenH8UbEjudOz8iakiMl3lE3lCVqYf2Y+znL8QFQ== dependencies: - defer-to-connect "^1.0.1" + "@aws-crypto/crc32" "3.0.0" + "@smithy/types" "^3.0.0" + "@smithy/util-hex-encoding" "^3.0.0" + tslib "^2.6.2" -"@szmarczak/http-timer@^4.0.5": - version "4.0.5" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152" - integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ== +"@smithy/eventstream-serde-browser@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-3.0.0.tgz#94721b01f01d8b7eb1db5814275a774ed4d38190" + integrity sha512-NB7AFiPN4NxP/YCAnrvYR18z2/ZsiHiF7VtG30gshO9GbFrIb1rC8ep4NGpJSWrz6P64uhPXeo4M0UsCLnZKqw== dependencies: - defer-to-connect "^2.0.0" + "@smithy/eventstream-serde-universal" "^3.0.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -"@tencent-sdk/capi@^1.1.2": - version "1.1.5" - resolved "https://registry.yarnpkg.com/@tencent-sdk/capi/-/capi-1.1.5.tgz#ba2932e292deb659d3e9968b70d9a6ec54d47c66" - integrity sha512-cHkoMY/1L5VxeiKv51uKxbFK8lZ7pZbY3CukzOHro8YKT6dETKYzTGO/F8jDhH7r8vKWxuA+ZcALzxYuVlmwsg== +"@smithy/eventstream-serde-config-resolver@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.0.0.tgz#420447d1d284d41f7f070a5d92fc3686cc922581" + integrity sha512-RUQG3vQ3LX7peqqHAbmayhgrF5aTilPnazinaSGF1P0+tgM3vvIRWPHmlLIz2qFqB9LqFIxditxc8O2Z6psrRw== dependencies: - "@types/request" "^2.48.3" - "@types/request-promise-native" "^1.0.17" - request "^2.88.0" - request-promise-native "^1.0.8" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -"@types/cacheable-request@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976" - integrity sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ== +"@smithy/eventstream-serde-node@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-3.0.0.tgz#6519523fbb429307be29b151b8ba35bcca2b6e64" + integrity sha512-baRPdMBDMBExZXIUAoPGm/hntixjt/VFpU6+VmCyiYJYzRHRxoaI1MN+5XE+hIS8AJ2GCHLMFEIOLzq9xx1EgQ== dependencies: - "@types/http-cache-semantics" "*" - "@types/keyv" "*" - "@types/node" "*" - "@types/responselike" "*" + "@smithy/eventstream-serde-universal" "^3.0.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -"@types/caseless@*": - version "0.12.2" - resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" - integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w== +"@smithy/eventstream-serde-universal@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-3.0.0.tgz#cb8441a73fbde4cbaa68e4a21236f658d914a073" + integrity sha512-HNFfShmotWGeAoW4ujP8meV9BZavcpmerDbPIjkJbxKbN8RsUcpRQ/2OyIxWNxXNH2GWCAxuSB7ynmIGJlQ3Dw== + dependencies: + "@smithy/eventstream-codec" "^3.0.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -"@types/http-cache-semantics@*": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a" - integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== +"@smithy/fetch-http-handler@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-3.0.1.tgz#dacfdf6e70d639fac4a0f57c42ce13f0ed14ff22" + integrity sha512-uaH74i5BDj+rBwoQaXioKpI0SHBJFtOVwzrCpxZxphOW0ki5jhj7dXvDMYM2IJem8TpdFvS2iC08sjOblfFGFg== + dependencies: + "@smithy/protocol-http" "^4.0.0" + "@smithy/querystring-builder" "^3.0.0" + "@smithy/types" "^3.0.0" + "@smithy/util-base64" "^3.0.0" + tslib "^2.6.2" -"@types/keyv@*": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7" - integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw== +"@smithy/hash-blob-browser@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/hash-blob-browser/-/hash-blob-browser-3.0.0.tgz#63ef4c98f74c53cbcad8ec73387c68ec4708f55b" + integrity sha512-/Wbpdg+bwJvW7lxR/zpWAc1/x/YkcqguuF2bAzkJrvXriZu1vm8r+PUdE4syiVwQg7PPR2dXpi3CLBb9qRDaVQ== dependencies: - "@types/node" "*" + "@smithy/chunked-blob-reader" "^3.0.0" + "@smithy/chunked-blob-reader-native" "^3.0.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -"@types/lodash@^4.14.123": - version "4.14.162" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.162.tgz#65d78c397e0d883f44afbf1f7ba9867022411470" - integrity sha512-alvcho1kRUnnD1Gcl4J+hK0eencvzq9rmzvFPRmP5rPHx9VVsJj6bKLTATPVf9ktgv4ujzh7T+XWKp+jhuODig== +"@smithy/hash-node@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/hash-node/-/hash-node-3.0.0.tgz#f44b5fff193e241c1cdcc957b296b60f186f0e59" + integrity sha512-84qXstNemP3XS5jcof0el6+bDfjzuvhJPQTEfro3lgtbCtKgzPm3MgiS6ehXVPjeQ5+JS0HqmTz8f/RYfzHVxw== + dependencies: + "@smithy/types" "^3.0.0" + "@smithy/util-buffer-from" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" -"@types/long@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" - integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== +"@smithy/hash-stream-node@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/hash-stream-node/-/hash-stream-node-3.0.0.tgz#b395a8a0d2427e4a8effc56135b37cb299339f8f" + integrity sha512-J0i7de+EgXDEGITD4fxzmMX8CyCNETTIRXlxjMiNUvvu76Xn3GJ31wQR85ynlPk2wI1lqoknAFJaD1fiNDlbIA== + dependencies: + "@smithy/types" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" -"@types/node@*": - version "14.11.10" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.10.tgz#8c102aba13bf5253f35146affbf8b26275069bef" - integrity sha512-yV1nWZPlMFpoXyoknm4S56y2nlTAuFYaJuQtYRAOU7xA/FJ9RY0Xm7QOkaYMMmr8ESdHIuUb6oQgR/0+2NqlyA== +"@smithy/invalid-dependency@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/invalid-dependency/-/invalid-dependency-3.0.0.tgz#21cb6b5203ee15321bfcc751f21f7a19536d4ae8" + integrity sha512-F6wBBaEFgJzj0s4KUlliIGPmqXemwP6EavgvDqYwCH40O5Xr2iMHvS8todmGVZtuJCorBkXsYLyTu4PuizVq5g== + dependencies: + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -"@types/node@^13.7.0": - version "13.13.26" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.26.tgz#09b8326828d46b174d29086cdb6dcd2d0dcf67a3" - integrity sha512-+48LLqolaKj/WnIY1crfLseaGQMIDISBy3PTXVOZ7w/PBaRUv+H8t94++atzfoBAvorbUYz6Xq9vh1fHrg33ig== +"@smithy/is-array-buffer@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz#9a95c2d46b8768946a9eec7f935feaddcffa5e7a" + integrity sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ== + dependencies: + tslib "^2.6.2" -"@types/request-promise-native@^1.0.17": - version "1.0.17" - resolved "https://registry.yarnpkg.com/@types/request-promise-native/-/request-promise-native-1.0.17.tgz#74a2d7269aebf18b9bdf35f01459cf0a7bfc7fab" - integrity sha512-05/d0WbmuwjtGMYEdHIBZ0tqMJJQ2AD9LG2F6rKNBGX1SSFR27XveajH//2N/XYtual8T9Axwl+4v7oBtPUZqg== +"@smithy/md5-js@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/md5-js/-/md5-js-3.0.0.tgz#6a2d1c496f4d4476a0fc84f7724d79b234c3eb13" + integrity sha512-Tm0vrrVzjlD+6RCQTx7D3Ls58S3FUH1ZCtU1MIh/qQmaOo1H9lMN2as6CikcEwgattnA9SURSdoJJ27xMcEfMA== dependencies: - "@types/request" "*" + "@smithy/types" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" -"@types/request@*", "@types/request@^2.48.3": - version "2.48.5" - resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.5.tgz#019b8536b402069f6d11bee1b2c03e7f232937a0" - integrity sha512-/LO7xRVnL3DxJ1WkPGDQrp4VTV1reX9RkC85mJ+Qzykj2Bdw+mG15aAfDahc76HtknjzE16SX/Yddn6MxVbmGQ== +"@smithy/middleware-content-length@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/middleware-content-length/-/middleware-content-length-3.0.0.tgz#084b3d22248967885d496eb0b105d9090e8ababd" + integrity sha512-3C4s4d/iGobgCtk2tnWW6+zSTOBg1PRAm2vtWZLdriwTroFbbWNSr3lcyzHdrQHnEXYCC5K52EbpfodaIUY8sg== dependencies: - "@types/caseless" "*" - "@types/node" "*" - "@types/tough-cookie" "*" - form-data "^2.5.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -"@types/responselike@*", "@types/responselike@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" - integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== +"@smithy/middleware-endpoint@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-3.0.1.tgz#49e8defb8e892e70417bd05f1faaf207070f32c7" + integrity sha512-lQ/UOdGD4KM5kLZiAl0q8Qy3dPbynvAXKAdXnYlrA1OpaUwr+neSsVokDZpY6ZVb5Yx8jnus29uv6XWpM9P4SQ== + dependencies: + "@smithy/middleware-serde" "^3.0.0" + "@smithy/node-config-provider" "^3.1.0" + "@smithy/shared-ini-file-loader" "^3.1.0" + "@smithy/types" "^3.0.0" + "@smithy/url-parser" "^3.0.0" + "@smithy/util-middleware" "^3.0.0" + tslib "^2.6.2" + +"@smithy/middleware-retry@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-3.0.3.tgz#8e9af1c9db4bc8904d73126225211b42b562f961" + integrity sha512-Wve1qzJb83VEU/6q+/I0cQdAkDnuzELC6IvIBwDzUEiGpKqXgX1v10FUuZGbRS6Ov/P+HHthcAoHOJZQvZNAkA== + dependencies: + "@smithy/node-config-provider" "^3.1.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/service-error-classification" "^3.0.0" + "@smithy/smithy-client" "^3.1.1" + "@smithy/types" "^3.0.0" + "@smithy/util-middleware" "^3.0.0" + "@smithy/util-retry" "^3.0.0" + tslib "^2.6.2" + uuid "^9.0.1" + +"@smithy/middleware-serde@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/middleware-serde/-/middleware-serde-3.0.0.tgz#786da6a6bc0e5e51d669dac834c19965245dd302" + integrity sha512-I1vKG1foI+oPgG9r7IMY1S+xBnmAn1ISqployvqkwHoSb8VPsngHDTOgYGYBonuOKndaWRUGJZrKYYLB+Ane6w== dependencies: - "@types/node" "*" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -"@types/tough-cookie@*": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d" - integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A== +"@smithy/middleware-stack@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/middleware-stack/-/middleware-stack-3.0.0.tgz#00f112bae7af5fc3bd37d4fab95ebce0f17a7774" + integrity sha512-+H0jmyfAyHRFXm6wunskuNAqtj7yfmwFB6Fp37enytp2q047/Od9xetEaUbluyImOlGnGpaVGaVfjwawSr+i6Q== + dependencies: + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -adm-zip@^0.4.13, adm-zip@^0.4.16: - version "0.4.16" - resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.16.tgz#cf4c508fdffab02c269cbc7f471a875f05570365" - integrity sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg== +"@smithy/node-config-provider@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@smithy/node-config-provider/-/node-config-provider-3.1.0.tgz#e962987c4e2e2b8b50397de5f4745eb21ee7bdbb" + integrity sha512-ngfB8QItUfTFTfHMvKuc2g1W60V1urIgZHqD1JNFZC2tTWXahqf2XvKXqcBS7yZqR7GqkQQZy11y/lNOUWzq7Q== + dependencies: + "@smithy/property-provider" "^3.1.0" + "@smithy/shared-ini-file-loader" "^3.1.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -after@0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" - integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= +"@smithy/node-http-handler@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-3.0.0.tgz#e771ea95d03e259f04b7b37e8aece8a4fffc8cdc" + integrity sha512-3trD4r7NOMygwLbUJo4eodyQuypAWr7uvPnebNJ9a70dQhVn+US8j/lCnvoJS6BXfZeF7PkkkI0DemVJw+n+eQ== + dependencies: + "@smithy/abort-controller" "^3.0.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/querystring-builder" "^3.0.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -agent-base@5: - version "5.1.1" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" - integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== +"@smithy/property-provider@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@smithy/property-provider/-/property-provider-3.1.0.tgz#b78d4964a1016b90331cc0c770b472160361fde7" + integrity sha512-Tj3+oVhqdZgemjCiWjFlADfhvLF4C/uKDuKo7/tlEsRQ9+3emCreR2xndj970QSRSsiCEU8hZW3/8JQu+n5w4Q== + dependencies: + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -agent-base@6: - version "6.0.1" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4" - integrity sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg== +"@smithy/protocol-http@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-4.0.0.tgz#04df3b5674b540323f678e7c4113e8abd8b26432" + integrity sha512-qOQZOEI2XLWRWBO9AgIYuHuqjZ2csyr8/IlgFDHDNuIgLAMRx2Bl8ck5U5D6Vh9DPdoaVpuzwWMa0xcdL4O/AQ== dependencies: - debug "4" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -ajv-keywords@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== +"@smithy/querystring-builder@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/querystring-builder/-/querystring-builder-3.0.0.tgz#48a9aa7b700e8409368c21bc0adf7564e001daea" + integrity sha512-bW8Fi0NzyfkE0TmQphDXr1AmBDbK01cA4C1Z7ggwMAU5RDz5AAv/KmoRwzQAS0kxXNf/D2ALTEgwK0U2c4LtRg== + dependencies: + "@smithy/types" "^3.0.0" + "@smithy/util-uri-escape" "^3.0.0" + tslib "^2.6.2" -ajv@^6.12.3, ajv@^6.12.6: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== +"@smithy/querystring-parser@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/querystring-parser/-/querystring-parser-3.0.0.tgz#fa1ed0cee408cd4d622070fa874bc50ac1a379b7" + integrity sha512-UzHwthk0UEccV4dHzPySnBy34AWw3V9lIqUTxmozQ+wPDAO9csCWMfOLe7V9A2agNYy7xE+Pb0S6K/J23JSzfQ== dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -ansi-align@^3.0.0: +"@smithy/service-error-classification@^3.0.0": version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb" - integrity sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw== + resolved "https://registry.yarnpkg.com/@smithy/service-error-classification/-/service-error-classification-3.0.0.tgz#06a45cb91b15b8b0d5f3b1df2b3743d2ca42f5c4" + integrity sha512-3BsBtOUt2Gsnc3X23ew+r2M71WwtpHfEDGhHYHSDg6q1t8FrWh15jT25DLajFV1H+PpxAJ6gqe9yYeRUsmSdFA== dependencies: - string-width "^3.0.0" + "@smithy/types" "^3.0.0" -ansi-bgblack@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-bgblack/-/ansi-bgblack-0.1.1.tgz#a68ba5007887701b6aafbe3fa0dadfdfa8ee3ca2" - integrity sha1-poulAHiHcBtqr74/oNrf36juPKI= +"@smithy/shared-ini-file-loader@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.0.tgz#a4cb9304c3be1c232ec661132ca88d177ac7a5b1" + integrity sha512-dAM7wSX0NR3qTNyGVN/nwwpEDzfV9T/3AN2eABExWmda5VqZKSsjlINqomO5hjQWGv+IIkoXfs3u2vGSNz8+Rg== dependencies: - ansi-wrap "0.1.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -ansi-bgblue@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-bgblue/-/ansi-bgblue-0.1.1.tgz#67bdc04edc9b9b5278969da196dea3d75c8c3613" - integrity sha1-Z73ATtybm1J4lp2hlt6j11yMNhM= +"@smithy/signature-v4@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-3.0.0.tgz#f536d0abebfeeca8e9aab846a4042658ca07d3b7" + integrity sha512-kXFOkNX+BQHe2qnLxpMEaCRGap9J6tUGLzc3A9jdn+nD4JdMwCKTJ+zFwQ20GkY+mAXGatyTw3HcoUlR39HwmA== + dependencies: + "@smithy/is-array-buffer" "^3.0.0" + "@smithy/types" "^3.0.0" + "@smithy/util-hex-encoding" "^3.0.0" + "@smithy/util-middleware" "^3.0.0" + "@smithy/util-uri-escape" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@smithy/smithy-client@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-3.1.1.tgz#9aa770edd9b6277dc4124c924c617a436cdb670e" + integrity sha512-tj4Ku7MpzZR8cmVuPcSbrLFVxmptWktmJMwST/uIEq4sarabEdF8CbmQdYB7uJ/X51Qq2EYwnRsoS7hdR4B7rA== dependencies: - ansi-wrap "0.1.0" + "@smithy/middleware-endpoint" "^3.0.1" + "@smithy/middleware-stack" "^3.0.0" + "@smithy/protocol-http" "^4.0.0" + "@smithy/types" "^3.0.0" + "@smithy/util-stream" "^3.0.1" + tslib "^2.6.2" -ansi-bgcyan@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-bgcyan/-/ansi-bgcyan-0.1.1.tgz#58489425600bde9f5507068dd969ebfdb50fe768" - integrity sha1-WEiUJWAL3p9VBwaN2Wnr/bUP52g= +"@smithy/types@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-3.0.0.tgz#00231052945159c64ffd8b91e8909d8d3006cb7e" + integrity sha512-VvWuQk2RKFuOr98gFhjca7fkBS+xLLURT8bUjk5XQoV0ZLm7WPwWPPY3/AwzTLuUBDeoKDCthfe1AsTUWaSEhw== dependencies: - ansi-wrap "0.1.0" + tslib "^2.6.2" -ansi-bggreen@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-bggreen/-/ansi-bggreen-0.1.1.tgz#4e3191248529943f4321e96bf131d1c13816af49" - integrity sha1-TjGRJIUplD9DIelr8THRwTgWr0k= +"@smithy/url-parser@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/url-parser/-/url-parser-3.0.0.tgz#5fdc77cd22051c1aac6531be0315bfcba0fa705d" + integrity sha512-2XLazFgUu+YOGHtWihB3FSLAfCUajVfNBXGGYjOaVKjLAuAxx3pSBY3hBgLzIgB17haf59gOG3imKqTy8mcrjw== dependencies: - ansi-wrap "0.1.0" + "@smithy/querystring-parser" "^3.0.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -ansi-bgmagenta@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-bgmagenta/-/ansi-bgmagenta-0.1.1.tgz#9b28432c076eaa999418672a3efbe19391c2c7a1" - integrity sha1-myhDLAduqpmUGGcqPvvhk5HCx6E= +"@smithy/util-base64@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-base64/-/util-base64-3.0.0.tgz#f7a9a82adf34e27a72d0719395713edf0e493017" + integrity sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ== dependencies: - ansi-wrap "0.1.0" + "@smithy/util-buffer-from" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" -ansi-bgred@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-bgred/-/ansi-bgred-0.1.1.tgz#a76f92838382ba43290a6c1778424f984d6f1041" - integrity sha1-p2+Sg4OCukMpCmwXeEJPmE1vEEE= +"@smithy/util-body-length-browser@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-body-length-browser/-/util-body-length-browser-3.0.0.tgz#86ec2f6256310b4845a2f064e2f571c1ca164ded" + integrity sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ== dependencies: - ansi-wrap "0.1.0" + tslib "^2.6.2" -ansi-bgwhite@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-bgwhite/-/ansi-bgwhite-0.1.1.tgz#6504651377a58a6ececd0331994e480258e11ba8" - integrity sha1-ZQRlE3elim7OzQMxmU5IAljhG6g= +"@smithy/util-body-length-node@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-body-length-node/-/util-body-length-node-3.0.0.tgz#99a291bae40d8932166907fe981d6a1f54298a6d" + integrity sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA== dependencies: - ansi-wrap "0.1.0" + tslib "^2.6.2" -ansi-bgyellow@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-bgyellow/-/ansi-bgyellow-0.1.1.tgz#c3fe2eb08cd476648029e6874d15a0b38f61d44f" - integrity sha1-w/4usIzUdmSAKeaHTRWgs49h1E8= +"@smithy/util-buffer-from@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz#559fc1c86138a89b2edaefc1e6677780c24594e3" + integrity sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA== dependencies: - ansi-wrap "0.1.0" + "@smithy/is-array-buffer" "^3.0.0" + tslib "^2.6.2" -ansi-black@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-black/-/ansi-black-0.1.1.tgz#f6185e889360b2545a1ec50c0bf063fc43032453" - integrity sha1-9hheiJNgslRaHsUMC/Bj/EMDJFM= +"@smithy/util-config-provider@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz#62c6b73b22a430e84888a8f8da4b6029dd5b8efe" + integrity sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ== dependencies: - ansi-wrap "0.1.0" + tslib "^2.6.2" -ansi-blue@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-blue/-/ansi-blue-0.1.1.tgz#15b804990e92fc9ca8c5476ce8f699777c21edbf" - integrity sha1-FbgEmQ6S/JyoxUds6PaZd3wh7b8= +"@smithy/util-defaults-mode-browser@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.3.tgz#6fff11a6c407ca1d5a1dc009768bd09271b199c2" + integrity sha512-3DFON2bvXJAukJe+qFgPV/rorG7ZD3m4gjCXHD1V5z/tgKQp5MCTCLntrd686tX6tj8Uli3lefWXJudNg5WmCA== dependencies: - ansi-wrap "0.1.0" + "@smithy/property-provider" "^3.1.0" + "@smithy/smithy-client" "^3.1.1" + "@smithy/types" "^3.0.0" + bowser "^2.11.0" + tslib "^2.6.2" -ansi-bold@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-bold/-/ansi-bold-0.1.1.tgz#3e63950af5acc2ae2e670e6f67deb115d1a5f505" - integrity sha1-PmOVCvWswq4uZw5vZ96xFdGl9QU= +"@smithy/util-defaults-mode-node@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.3.tgz#0b52ba9cb1138ee9076feba9a733462b2e2e6093" + integrity sha512-D0b8GJXecT00baoSQ3Iieu3k3mZ7GY8w1zmg8pdogYrGvWJeLcIclqk2gbkG4K0DaBGWrO6v6r20iwIFfDYrmA== + dependencies: + "@smithy/config-resolver" "^3.0.1" + "@smithy/credential-provider-imds" "^3.1.0" + "@smithy/node-config-provider" "^3.1.0" + "@smithy/property-provider" "^3.1.0" + "@smithy/smithy-client" "^3.1.1" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" + +"@smithy/util-endpoints@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@smithy/util-endpoints/-/util-endpoints-2.0.1.tgz#4ea8069bfbf3ebbcbe106b5156ff59a7a627b7dd" + integrity sha512-ZRT0VCOnKlVohfoABMc8lWeQo/JEFuPWctfNRXgTHbyOVssMOLYFUNWukxxiHRGVAhV+n3c0kPW+zUqckjVPEA== dependencies: - ansi-wrap "0.1.0" + "@smithy/node-config-provider" "^3.1.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -ansi-colors@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-0.2.0.tgz#72c31de2a0d9a2ccd0cac30cc9823eeb2f6434b5" - integrity sha1-csMd4qDZoszQysMMyYI+6y9kNLU= - dependencies: - ansi-bgblack "^0.1.1" - ansi-bgblue "^0.1.1" - ansi-bgcyan "^0.1.1" - ansi-bggreen "^0.1.1" - ansi-bgmagenta "^0.1.1" - ansi-bgred "^0.1.1" - ansi-bgwhite "^0.1.1" - ansi-bgyellow "^0.1.1" - ansi-black "^0.1.1" - ansi-blue "^0.1.1" - ansi-bold "^0.1.1" - ansi-cyan "^0.1.1" - ansi-dim "^0.1.1" - ansi-gray "^0.1.1" - ansi-green "^0.1.1" - ansi-grey "^0.1.1" - ansi-hidden "^0.1.1" - ansi-inverse "^0.1.1" - ansi-italic "^0.1.1" - ansi-magenta "^0.1.1" - ansi-red "^0.1.1" - ansi-reset "^0.1.1" - ansi-strikethrough "^0.1.1" - ansi-underline "^0.1.1" - ansi-white "^0.1.1" - ansi-yellow "^0.1.1" - lazy-cache "^2.0.1" - -ansi-cyan@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-cyan/-/ansi-cyan-0.1.1.tgz#538ae528af8982f28ae30d86f2f17456d2609873" - integrity sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM= +"@smithy/util-hex-encoding@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz#32938b33d5bf2a15796cd3f178a55b4155c535e6" + integrity sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ== dependencies: - ansi-wrap "0.1.0" + tslib "^2.6.2" -ansi-dim@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-dim/-/ansi-dim-0.1.1.tgz#40de4c603aa8086d8e7a86b8ff998d5c36eefd6c" - integrity sha1-QN5MYDqoCG2Oeoa4/5mNXDbu/Ww= +"@smithy/util-middleware@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-3.0.0.tgz#64d775628b99a495ca83ce982f5c83aa45f1e894" + integrity sha512-q5ITdOnV2pXHSVDnKWrwgSNTDBAMHLptFE07ua/5Ty5WJ11bvr0vk2a7agu7qRhrCFRQlno5u3CneU5EELK+DQ== dependencies: - ansi-wrap "0.1.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -ansi-escapes@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" - integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== +"@smithy/util-retry@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-3.0.0.tgz#8a0c47496aab74e1dfde4905d462ad636a8824bb" + integrity sha512-nK99bvJiziGv/UOKJlDvFF45F00WgPLKVIGUfAK+mDhzVN2hb/S33uW2Tlhg5PVBoqY7tDVqL0zmu4OxAHgo9g== + dependencies: + "@smithy/service-error-classification" "^3.0.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -ansi-escapes@^4.2.1, ansi-escapes@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" - integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== +"@smithy/util-stream@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-3.0.1.tgz#3cf527bcd3fec82c231c38d47dd75f3364747edb" + integrity sha512-7F7VNNhAsfMRA8I986YdOY5fE0/T1/ZjFF6OLsqkvQVNP3vZ/szYDfGCyphb7ioA09r32K/0qbSFfNFU68aSzA== + dependencies: + "@smithy/fetch-http-handler" "^3.0.1" + "@smithy/node-http-handler" "^3.0.0" + "@smithy/types" "^3.0.0" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-buffer-from" "^3.0.0" + "@smithy/util-hex-encoding" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@smithy/util-uri-escape@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz#e43358a78bf45d50bb736770077f0f09195b6f54" + integrity sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg== dependencies: - type-fest "^0.11.0" + tslib "^2.6.2" -ansi-gray@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" - integrity sha1-KWLPVOyXksSFEKPetSRDaGHvclE= +"@smithy/util-utf8@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-utf8/-/util-utf8-3.0.0.tgz#1a6a823d47cbec1fd6933e5fc87df975286d9d6a" + integrity sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA== dependencies: - ansi-wrap "0.1.0" + "@smithy/util-buffer-from" "^3.0.0" + tslib "^2.6.2" -ansi-green@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-green/-/ansi-green-0.1.1.tgz#8a5d9a979e458d57c40e33580b37390b8e10d0f7" - integrity sha1-il2al55FjVfEDjNYCzc5C44Q0Pc= +"@smithy/util-waiter@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-waiter/-/util-waiter-3.0.0.tgz#26bcc5bbbf1de9360a7aeb3b3919926fc6afa2bc" + integrity sha512-+fEXJxGDLCoqRKVSmo0auGxaqbiCo+8oph+4auefYjaNxjOLKSY2MxVQfRzo65PaZv4fr+5lWg+au7vSuJJ/zw== dependencies: - ansi-wrap "0.1.0" + "@smithy/abort-controller" "^3.0.0" + "@smithy/types" "^3.0.0" + tslib "^2.6.2" -ansi-grey@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-grey/-/ansi-grey-0.1.1.tgz#59d98b6ac2ba19f8a51798e9853fba78339a33c1" - integrity sha1-WdmLasK6GfilF5jphT+6eDOaM8E= +"@szmarczak/http-timer@^4.0.5": + version "4.0.5" + resolved "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz" + integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ== dependencies: - ansi-wrap "0.1.0" + defer-to-connect "^2.0.0" -ansi-hidden@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-hidden/-/ansi-hidden-0.1.1.tgz#ed6a4c498d2bb7cbb289dbf2a8d1dcc8567fae0f" - integrity sha1-7WpMSY0rt8uyidvyqNHcyFZ/rg8= +"@tokenizer/token@^0.3.0": + version "0.3.0" + resolved "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz" + integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A== + +"@types/cacheable-request@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz" + integrity sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ== dependencies: - ansi-wrap "0.1.0" + "@types/http-cache-semantics" "*" + "@types/keyv" "*" + "@types/node" "*" + "@types/responselike" "*" -ansi-inverse@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-inverse/-/ansi-inverse-0.1.1.tgz#b6af45826fe826bfb528a6c79885794355ccd269" - integrity sha1-tq9Fgm/oJr+1KKbHmIV5Q1XM0mk= +"@types/find-cache-dir@^3.2.1": + version "3.2.1" + resolved "https://registry.npmjs.org/@types/find-cache-dir/-/find-cache-dir-3.2.1.tgz" + integrity sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw== + +"@types/fs-extra@^11.0.1": + version "11.0.1" + resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.1.tgz" + integrity sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA== dependencies: - ansi-wrap "0.1.0" + "@types/jsonfile" "*" + "@types/node" "*" -ansi-italic@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-italic/-/ansi-italic-0.1.1.tgz#104743463f625c142a036739cf85eda688986f23" - integrity sha1-EEdDRj9iXBQqA2c5z4XtpoiYbyM= +"@types/http-cache-semantics@*": + version "4.0.0" + resolved "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz" + integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== + +"@types/jsonfile@*": + version "6.1.1" + resolved "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.1.tgz" + integrity sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png== dependencies: - ansi-wrap "0.1.0" + "@types/node" "*" -ansi-magenta@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-magenta/-/ansi-magenta-0.1.1.tgz#063b5ba16fb3f23e1cfda2b07c0a89de11e430ae" - integrity sha1-BjtboW+z8j4c/aKwfAqJ3hHkMK4= +"@types/keyv@*": + version "3.1.1" + resolved "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz" + integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw== dependencies: - ansi-wrap "0.1.0" + "@types/node" "*" -ansi-red@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-red/-/ansi-red-0.1.1.tgz#8c638f9d1080800a353c9c28c8a81ca4705d946c" - integrity sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw= +"@types/lodash-es@^4.17.6": + version "4.17.7" + resolved "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.7.tgz" + integrity sha512-z0ptr6UI10VlU6l5MYhGwS4mC8DZyYer2mCoyysZtSF7p26zOX8UpbrV0YpNYLGS8K4PUFIyEr62IMFFjveSiQ== dependencies: - ansi-wrap "0.1.0" + "@types/lodash" "*" -ansi-regex@^2.0.0, ansi-regex@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= +"@types/lodash@*": + version "4.14.194" + resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.194.tgz" + integrity sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g== -ansi-regex@^3.0.0: +"@types/lodash@^4.14.123": + version "4.17.5" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.5.tgz#e6c29b58e66995d57cd170ce3e2a61926d55ee04" + integrity sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw== + +"@types/node@*": + version "14.11.10" + resolved "https://registry.npmjs.org/@types/node/-/node-14.11.10.tgz" + integrity sha512-yV1nWZPlMFpoXyoknm4S56y2nlTAuFYaJuQtYRAOU7xA/FJ9RY0Xm7QOkaYMMmr8ESdHIuUb6oQgR/0+2NqlyA== + +"@types/responselike@*", "@types/responselike@^1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz" + integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== + dependencies: + "@types/node" "*" + +"@types/semver@^7.3.13": + version "7.3.13" + resolved "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz" + integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== + +"@types/yarnpkg__lockfile@^1.1.5": + version "1.1.5" + resolved "https://registry.npmjs.org/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.5.tgz" + integrity sha512-8NYnGOctzsI4W0ApsP/BIHD/LnxpJ6XaGf2AZmz4EyDYJMxtprN4279dLNI1CPZcwC9H18qYcaFv4bXi0wmokg== + +"@yarnpkg/lockfile@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz" + integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== + +abort-controller@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== +adm-zip@^0.5.5: + version "0.5.14" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.14.tgz#2c557c0bf12af4311cf6d32970f4060cf8133b2a" + integrity sha512-DnyqqifT4Jrcvb8USYjp6FHtBpEIz1mnXu6pTRHZ0RL69LbQYiO+0lDFg5+OKA7U29oWSs3a/i8fhn8ZcceIWg== -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" -ansi-reset@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-reset/-/ansi-reset-0.1.1.tgz#e7e71292c3c7ddcd4d62ef4a6c7c05980911c3b7" - integrity sha1-5+cSksPH3c1NYu9KbHwFmAkRw7c= +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== dependencies: - ansi-wrap "0.1.0" + ajv "^8.0.0" -ansi-strikethrough@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-strikethrough/-/ansi-strikethrough-0.1.1.tgz#d84877140b2cff07d1c93ebce69904f68885e568" - integrity sha1-2Eh3FAss/wfRyT685pkE9oiF5Wg= +ajv@^8.0.0, ajv@^8.12.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.16.0.tgz#22e2a92b94f005f7e0f9c9d39652ef0b8f6f0cb4" + integrity sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw== dependencies: - ansi-wrap "0.1.0" + fast-deep-equal "^3.1.3" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.4.1" + +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-styles@^3.2.1: version "3.2.1" @@ -726,60 +1731,29 @@ ansi-styles@^3.2.1: ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" -ansi-underline@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-underline/-/ansi-underline-0.1.1.tgz#dfc920f4c97b5977ea162df8ffb988308aaa71a4" - integrity sha1-38kg9Ml7WXfqFi34/7mIMIqqcaQ= - dependencies: - ansi-wrap "0.1.0" - -ansi-white@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-white/-/ansi-white-0.1.1.tgz#9c77b7c193c5ee992e6011d36ec4c921b4578944" - integrity sha1-nHe3wZPF7pkuYBHTbsTJIbRXiUQ= - dependencies: - ansi-wrap "0.1.0" - -ansi-wrap@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" - integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= - -ansi-yellow@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-yellow/-/ansi-yellow-0.1.1.tgz#cb9356f2f46c732f0e3199e6102955a77da83c1d" - integrity sha1-y5NW8vRscy8OMZnmEClVp32oPB0= - dependencies: - ansi-wrap "0.1.0" - -anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - archive-type@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/archive-type/-/archive-type-4.0.0.tgz#f92e72233056dfc6969472749c267bdb046b1d70" - integrity sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA= + resolved "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz" + integrity sha512-zV4Ky0v1F8dBrdYElwTvQhweQ0P7Kwc1aluqJsYtOBP01jXcWCyW2IEfI1YiqsG+Iy7ZR+o5LF1N+PGECBxHWA== dependencies: file-type "^4.2.0" archiver-utils@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2" + resolved "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz" integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw== dependencies: glob "^7.1.4" @@ -793,9 +1767,25 @@ archiver-utils@^2.1.0: normalize-path "^3.0.0" readable-stream "^2.0.0" +archiver-utils@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-3.0.4.tgz#a0d201f1cf8fce7af3b5a05aea0a337329e96ec7" + integrity sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw== + dependencies: + glob "^7.2.3" + graceful-fs "^4.2.0" + lazystream "^1.0.0" + lodash.defaults "^4.2.0" + lodash.difference "^4.5.0" + lodash.flatten "^4.4.0" + lodash.isplainobject "^4.0.6" + lodash.union "^4.6.0" + normalize-path "^3.0.0" + readable-stream "^3.6.0" + archiver@^3.0.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/archiver/-/archiver-3.1.1.tgz#9db7819d4daf60aec10fe86b16cb9258ced66ea0" + resolved "https://registry.npmjs.org/archiver/-/archiver-3.1.1.tgz" integrity sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg== dependencies: archiver-utils "^2.1.0" @@ -806,39 +1796,18 @@ archiver@^3.0.0: tar-stream "^2.1.0" zip-stream "^2.1.2" -archiver@^5.0.0, archiver@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.0.2.tgz#b2c435823499b1f46eb07aa18e7bcb332f6ca3fc" - integrity sha512-Tq3yV/T4wxBsD2Wign8W9VQKhaUxzzRmjEiSoOK0SLqPgDP/N1TKdYyBeIEu56T4I9iO4fKTTR0mN9NWkBA0sg== - dependencies: - archiver-utils "^2.1.0" - async "^3.2.0" - buffer-crc32 "^0.2.1" - readable-stream "^3.6.0" - readdir-glob "^1.0.0" - tar-stream "^2.1.4" - zip-stream "^4.0.0" - -archiver@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.2.0.tgz#25aa1b3d9febf7aec5b0f296e77e69960c26db94" - integrity sha512-QEAKlgQuAtUxKeZB9w5/ggKXh21bZS+dzzuQ0RPBC20qtDCbTyzqmisoeJP46MP39fg4B4IcyvR+yeyEBdblsQ== +archiver@^5.3.0, archiver@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.2.tgz#99991d5957e53bd0303a392979276ac4ddccf3b0" + integrity sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw== dependencies: archiver-utils "^2.1.0" - async "^3.2.0" + async "^3.2.4" buffer-crc32 "^0.2.1" readable-stream "^3.6.0" - readdir-glob "^1.0.0" - tar-stream "^2.1.4" - zip-stream "^4.0.4" - -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" + readdir-glob "^1.1.2" + tar-stream "^2.2.0" + zip-stream "^4.1.0" argparse@^1.0.7: version "1.0.10" @@ -849,245 +1818,216 @@ argparse@^1.0.7: argparse@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz" + integrity sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q== -arr-swap@^1.0.1: +array-buffer-byte-length@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/arr-swap/-/arr-swap-1.0.1.tgz#147590ed65fc815bc07fef0997c2e5823d643534" - integrity sha1-FHWQ7WX8gVvAf+8Jl8Llgj1kNTQ= + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== dependencies: - is-number "^3.0.0" + call-bind "^1.0.5" + is-array-buffer "^3.0.4" array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -arraybuffer.slice@~0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" - integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog== - -asn1@~0.2.3: - version "0.2.4" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - -async-limiter@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== - -async@^2.6.1, async@^2.6.2, async@^2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" + is-shared-array-buffer "^1.0.2" + +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + +async@^2.6.3: + version "2.6.4" + resolved "https://registry.npmjs.org/async/-/async-2.6.4.tgz" + integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== dependencies: lodash "^4.17.14" -async@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" - integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== +async@^3.2.4: + version "3.2.5" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== asynckit@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== at-least-node@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -aws-sdk@^2.828.0: - version "2.828.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.828.0.tgz#6aa599c3582f219568f41fb287eb65753e4a9234" - integrity sha512-JoDujGdncSIF9ka+XFZjop/7G+fNGucwPwYj7OHYMmFIOV5p7YmqomdbVmH/vIzd988YZz8oLOinWc4jM6vvhg== +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + +aws-sdk@^2.1329.0, aws-sdk@^2.1404.0: + version "2.1638.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1638.0.tgz#b17eccbcaa609faadbb088bbdfbb944756ee3e13" + integrity sha512-/Li+eOMvJOLuYXimt3YPd6ec9Xvzh6L5KLfU5bjuJrltQqBcW7paL+PnFqSjm7zef+fPJT7h+8sqEcuRaGUmRA== dependencies: buffer "4.9.2" events "1.1.1" ieee754 "1.1.13" - jmespath "0.15.0" + jmespath "0.16.0" querystring "0.2.0" sax "1.2.1" url "0.10.3" - uuid "3.3.2" - xml2js "0.4.19" + util "^0.12.4" + uuid "8.0.0" + xml2js "0.6.2" -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= - -aws4@^1.11.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" - integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== - -aws4@^1.8.0: - version "1.10.1" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.1.tgz#e1e82e4f3e999e2cfd61b161280d16a111f86428" - integrity sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA== - -axios@^0.21.1: - version "0.21.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" - integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== +axios@^0.28.0, axios@^1.6.2: + version "0.28.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.28.1.tgz#2a7bcd34a3837b71ee1a5ca3762214b86b703e70" + integrity sha512-iUcGA5a7p0mVb4Gm/sy+FSECNkPFT4y7wt6OM/CDpO/OnNCvSs3PoMG8ibrC9jRoGYU0gUK5pXVC4NPXq6lHRQ== dependencies: - follow-redirects "^1.10.0" - -backo2@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" - integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -base64-arraybuffer@0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812" - integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI= + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base64-js@^1.0.2: version "1.3.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz" integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= +bash-glob@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/bash-glob/-/bash-glob-2.0.0.tgz" + integrity sha512-53/NJ+t2UAkEYgQPO6aFjbx1Ue8vNNXCYaA4EljNKP1SR8A9dSQQoBmYWR8BLXO0/NDRJEMSJ4BxWihi//m3Kw== dependencies: - tweetnacl "^0.14.3" - -binary-extensions@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" - integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== + bash-path "^1.0.1" + component-emitter "^1.2.1" + cross-spawn "^5.1.0" + each-parallel-async "^1.0.0" + extend-shallow "^2.0.1" + is-extglob "^2.1.1" + is-glob "^4.0.0" -binary@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" - integrity sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk= +bash-path@^1.0.1: + version "1.0.3" + resolved "https://registry.npmjs.org/bash-path/-/bash-path-1.0.3.tgz" + integrity sha512-mGrYvOa6yTY/qNCiZkPFJqWmODK68y6kmVRAJ1NNbWlNoJrUrsFxu7FU2EKg7gbrer6ttrKkF2s/E/lhRy7/OA== dependencies: - buffers "~0.1.1" - chainsaw "~0.1.0" + arr-union "^3.1.0" + is-windows "^1.0.1" -bindings@^1.3.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== bl@^1.0.0: version "1.2.3" - resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7" + resolved "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz" integrity sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww== dependencies: readable-stream "^2.3.5" safe-buffer "^5.1.1" -bl@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.1.tgz#8c11a7b730655c5d56898cdc871224f40fd901d5" - integrity sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g== - dependencies: - readable-stream "^2.3.5" - safe-buffer "^5.1.1" - bl@^4.0.3: version "4.0.3" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" + resolved "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz" integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg== dependencies: buffer "^5.5.0" inherits "^2.0.4" readable-stream "^3.4.0" -blob@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" - integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" -bluebird@^3.4.7, bluebird@^3.5.3, bluebird@^3.7.2: +bluebird@^3.5.3, bluebird@^3.7.2: version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -boxen@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.0.0.tgz#64fe9b16066af815f51057adcc800c3730120854" - integrity sha512-5bvsqw+hhgUi3oYGK0Vf4WpIkyemp60WBInn7+WNfoISzAqk/HX4L7WNROq38E6UR/y3YADpv6pEm4BfkeEAdA== - dependencies: - ansi-align "^3.0.0" - camelcase "^6.2.0" - chalk "^4.1.0" - cli-boxes "^2.2.1" - string-width "^4.2.0" - type-fest "^0.20.2" - widest-line "^3.1.0" - wrap-ansi "^7.0.0" +bowser@^2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" + integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== brace-expansion@^1.1.7: version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^3.0.1, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.1, braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" buffer-alloc-unsafe@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" + resolved "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz" integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== buffer-alloc@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" + resolved "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz" integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== dependencies: buffer-alloc-unsafe "^1.1.0" buffer-fill "^1.0.0" -buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3, buffer-crc32@~0.2.5: +buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= buffer-fill@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" - integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= - -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + resolved "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz" + integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ== buffer@4.9.2: version "4.9.2" @@ -1100,96 +2040,57 @@ buffer@4.9.2: buffer@^5.1.0, buffer@^5.2.1, buffer@^5.5.0: version "5.6.0" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" + resolved "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz" integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== dependencies: base64-js "^1.0.2" ieee754 "^1.1.4" -buffermaker@~1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/buffermaker/-/buffermaker-1.2.1.tgz#0631f92b891a84b750f1036491ac857c734429f4" - integrity sha512-IdnyU2jDHU65U63JuVQNTHiWjPRH0CS3aYd/WPaEwyX84rFdukhOduAVb1jwUScmb5X0JWPw8NZOrhoLMiyAHQ== - dependencies: - long "1.1.2" - -buffers@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" - integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s= +builtin-modules@^3.3.0: + version "3.3.0" + resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== -builtin-modules@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.1.0.tgz#aad97c15131eb76b65b50ef208e7584cd76a7484" - integrity sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw== +builtins@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" + integrity sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ== cacheable-lookup@^5.0.3: version "5.0.3" - resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz#049fdc59dffdd4fc285e8f4f82936591bd59fec3" + resolved "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz" integrity sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w== -cacheable-request@^2.1.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d" - integrity sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0= - dependencies: - clone-response "1.0.2" - get-stream "3.0.0" - http-cache-semantics "3.8.1" - keyv "3.0.0" - lowercase-keys "1.0.0" - normalize-url "2.0.1" - responselike "1.0.2" - -cacheable-request@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" - integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^3.0.0" - lowercase-keys "^2.0.0" - normalize-url "^4.1.0" - responselike "^1.0.2" - -cacheable-request@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.1.tgz#062031c2856232782ed694a257fa35da93942a58" - integrity sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw== +cacheable-request@^7.0.2: + version "7.0.2" + resolved "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz" + integrity sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew== dependencies: clone-response "^1.0.2" get-stream "^5.1.0" http-cache-semantics "^4.0.0" keyv "^4.0.0" lowercase-keys "^2.0.0" - normalize-url "^4.1.0" + normalize-url "^6.0.1" responselike "^2.0.0" cachedir@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" - integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== - -camelcase@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" - integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= - -chainsaw@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" - integrity sha1-XqtQsor+WAdNDVgpE4iCi15fvJg= - dependencies: - traverse ">=0.3.0 <0.4" - -chalk@^2.0.1, chalk@^2.4.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.4.0.tgz#7fef9cf7367233d7c88068fe6e34ed0d355a610d" + integrity sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ== + +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +chalk@^2.4.1: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -1198,17 +2099,30 @@ chalk@^2.0.1, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" - integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz" + integrity sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA== + chardet@^0.7.0: version "0.7.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== child-process-ext@^2.1.1: @@ -1222,136 +2136,106 @@ child-process-ext@^2.1.1: split2 "^3.1.1" stream-promise "^3.2.0" -choices-separator@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/choices-separator/-/choices-separator-2.0.0.tgz#92fd1763182d79033f5c5c51d0ba352e5567c696" - integrity sha1-kv0XYxgteQM/XFxR0Lo1LlVnxpY= - dependencies: - ansi-dim "^0.1.1" - debug "^2.6.6" - strip-color "^0.1.0" - -chokidar@^3.4.1: - version "3.4.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b" - integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ== +child-process-ext@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/child-process-ext/-/child-process-ext-3.0.2.tgz#701b77a3a27b8eefdf7264d8350b29c3a9cbba32" + integrity sha512-oBePsLbQpTJFxzwyCvs9yWWF0OEM6vGGepHwt1stqmX7QQqOuDc8j2ywdvAs9Tvi44TT7d9ackqhR4Q10l1u8w== dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.5.0" - optionalDependencies: - fsevents "~2.1.2" + cross-spawn "^7.0.3" + es5-ext "^0.10.62" + log "^6.3.1" + split2 "^3.2.2" + stream-promise "^3.2.0" -chokidar@^3.5.0: - version "3.5.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" - integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== +chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== dependencies: - anymatch "~3.1.1" + anymatch "~3.1.2" braces "~3.0.2" - glob-parent "~5.1.0" + glob-parent "~5.1.2" is-binary-path "~2.1.0" is-glob "~4.0.1" normalize-path "~3.0.0" - readdirp "~3.5.0" + readdirp "~3.6.0" optionalDependencies: - fsevents "~2.3.1" - -chownr@^1.0.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + fsevents "~2.3.2" chownr@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== -cli-boxes@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" - integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== +ci-info@^3.8.0: + version "3.8.0" + resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz" + integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== -cli-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-2.0.0.tgz#11ecfb58a79278cf6035a60c54e338f9d837897c" - integrity sha512-a0VZ8LeraW0jTuCkuAGMNufareGHhyZU9z8OGsW0gXd1hZGi1SRuNRXdbGkraBBKnhyUhyebFWnRbp+dIn0f0A== +cli-color@^2.0.1, cli-color@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/cli-color/-/cli-color-2.0.3.tgz" + integrity sha512-OkoZnxyC4ERN3zLzZaY9Emb7f/MhBOIpePv0Ycok0fJYT+Ouo00UBEIwsVsr0yoow++n5YWlSUgST9GKhNHiRQ== dependencies: - ansi-regex "^2.1.1" d "^1.0.1" - es5-ext "^0.10.51" + es5-ext "^0.10.61" es6-iterator "^2.0.3" - memoizee "^0.4.14" + memoizee "^0.4.15" timers-ext "^0.1.7" -cli-cursor@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" - integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= - dependencies: - restore-cursor "^2.0.0" - cli-cursor@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz" integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== dependencies: restore-cursor "^3.1.0" -cli-width@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" - integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== +cli-progress-footer@^2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/cli-progress-footer/-/cli-progress-footer-2.3.2.tgz" + integrity sha512-uzHGgkKdeA9Kr57eyH1W5HGiNShP8fV1ETq04HDNM1Un6ShXbHhwi/H8LNV9L1fQXKjEw0q5FUkEVNuZ+yZdSw== + dependencies: + cli-color "^2.0.2" + d "^1.0.1" + es5-ext "^0.10.61" + mute-stream "0.0.8" + process-utils "^4.0.0" + timers-ext "^0.1.7" + type "^2.6.0" -cli-width@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" - integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== +cli-spinners@^2.5.0: + version "2.8.0" + resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.8.0.tgz" + integrity sha512-/eG5sJcvEIwxcdYM86k5tPwn0MUzkX5YY3eImTGpJOZgVe4SdTMY14vQpcxgBzJ0wXwAYrS8E+c3uHeK4JNyzQ== -clone-deep@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-1.0.0.tgz#b2f354444b5d4a0ce58faca337ef34da2b14a6c7" - integrity sha512-hmJRX8x1QOJVV+GUjOBzi6iauhPqc9hIF6xitWRBbiPZOBb6vGo/mDRIK9P74RTKSQK7AE8B0DDWY/vpRrPmQw== +cli-sprintf-format@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/cli-sprintf-format/-/cli-sprintf-format-1.1.1.tgz" + integrity sha512-BbEjY9BEdA6wagVwTqPvmAwGB24U93rQPBFZUT8lNCDxXzre5LFHQUTJc70czjgUomVg8u8R5kW8oY9DYRFNeg== dependencies: - for-own "^1.0.0" - is-plain-object "^2.0.4" - kind-of "^5.0.0" - shallow-clone "^1.0.0" + cli-color "^2.0.1" + es5-ext "^0.10.53" + sprintf-kit "^2.0.1" + supports-color "^6.1.0" -clone-deep@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== -clone-response@1.0.2, clone-response@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" - integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= +clone-response@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" + integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== dependencies: mimic-response "^1.0.0" -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== -color-convert@^1.9.0, color-convert@^1.9.1: +color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -1360,7 +2244,7 @@ color-convert@^1.9.0, color-convert@^1.9.1: color-convert@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: color-name "~1.1.4" @@ -1368,92 +2252,63 @@ color-convert@^2.0.1: color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== -color-name@^1.0.0, color-name@~1.1.4: +color-name@~1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.5.2: - version "1.5.4" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6" - integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color@3.0.x: - version "3.0.0" - resolved "https://registry.yarnpkg.com/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a" - integrity sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w== - dependencies: - color-convert "^1.9.1" - color-string "^1.5.2" - -colornames@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/colornames/-/colornames-1.1.1.tgz#f8889030685c7c4ff9e2a559f5077eb76a816f96" - integrity sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y= - -colors@1.3.x: - version "1.3.3" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d" - integrity sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg== - -colors@^1.2.1: +colors@1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + resolved "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== -colorspace@1.1.x: - version "1.1.2" - resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.2.tgz#e0128950d082b86a2168580796a0aa5d6c68d8c5" - integrity sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ== - dependencies: - color "3.0.x" - text-hex "1.0.x" - -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.8: version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" -commander@2.19.x: - version "2.19.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" - integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== -commander@^2.8.1: +commander@^2.11.0, commander@^2.8.1: version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + commander@~4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== -component-bind@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" - integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E= +common-path-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz" + integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w== -component-emitter@^1.2.0, component-emitter@^1.2.1, component-emitter@~1.3.0: +component-emitter@^1.2.1: version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + resolved "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== -component-inherit@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" - integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM= +component-emitter@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17" + integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ== compress-commons@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-2.1.1.tgz#9410d9a534cf8435e3fbbb7c6ce48de2dc2f0610" + resolved "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz" integrity sha512-eVw6n7CnEMFzc3duyFVrQEuY1BlHR3rYsSztyG32ibGMW722i3C6IizEGMFmfMU+A+fALvBIwxN3czffTcdA+Q== dependencies: buffer-crc32 "^0.2.13" @@ -1461,97 +2316,75 @@ compress-commons@^2.1.1: normalize-path "^3.0.0" readable-stream "^2.3.6" -compress-commons@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.0.1.tgz#c5fa908a791a0c71329fba211d73cd2a32005ea8" - integrity sha512-xZm9o6iikekkI0GnXCmAl3LQGZj5TBDj0zLowsqi7tJtEa3FMGSEcHcqrSJIrOAk1UG/NBbDn/F1q+MG/p/EsA== - dependencies: - buffer-crc32 "^0.2.13" - crc32-stream "^4.0.0" - normalize-path "^3.0.0" - readable-stream "^3.6.0" - -compress-commons@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.0.2.tgz#d6896be386e52f37610cef9e6fa5defc58c31bd7" - integrity sha512-qhd32a9xgzmpfoga1VQEiLEwdKZ6Plnpx5UCgIsf89FSolyJ7WnifY4Gtjgv5WR6hWAyRaHxC5MiEhU/38U70A== +compress-commons@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.2.tgz#6542e59cb63e1f46a8b21b0e06f9a32e4c8b06df" + integrity sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg== dependencies: buffer-crc32 "^0.2.13" - crc32-stream "^4.0.1" + crc32-stream "^4.0.2" normalize-path "^3.0.0" readable-stream "^3.6.0" concat-map@0.0.1: version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -content-disposition@^0.5.2: - version "0.5.3" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== +content-disposition@^0.5.4: + version "0.5.4" + resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== dependencies: - safe-buffer "5.1.2" - -cookiejar@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" - integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA== + safe-buffer "5.2.1" -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= +cookiejar@^2.1.3, cookiejar@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" + integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== -core-util-is@1.0.2, core-util-is@~1.0.0: +core-util-is@~1.0.0: version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= crc-32@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208" - integrity sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA== - dependencies: - exit-on-epipe "~1.0.1" - printj "~1.1.0" + version "1.2.2" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== crc32-stream@^3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-3.0.1.tgz#cae6eeed003b0e44d739d279de5ae63b171b4e85" + resolved "https://registry.npmjs.org/crc32-stream/-/crc32-stream-3.0.1.tgz" integrity sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w== dependencies: crc "^3.4.4" readable-stream "^3.4.0" -crc32-stream@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.0.tgz#05b7ca047d831e98c215538666f372b756d91893" - integrity sha512-tyMw2IeUX6t9jhgXI6um0eKfWq4EIDpfv5m7GX4Jzp7eVelQ360xd8EPXJhp2mHwLQIkqlnMLjzqSZI3a+0wRw== - dependencies: - crc "^3.4.4" - readable-stream "^3.4.0" - -crc32-stream@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.1.tgz#0f047d74041737f8a55e86837a1b826bd8ab0067" - integrity sha512-FN5V+weeO/8JaXsamelVYO1PHyeCsuL3HcG4cqsj0ceARcocxalaShCsohZMSAF+db7UYFwBy1rARK/0oFItUw== +crc32-stream@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.3.tgz#85dd677eb78fa7cad1ba17cc506a597d41fc6f33" + integrity sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw== dependencies: crc-32 "^1.2.0" readable-stream "^3.4.0" crc@^3.4.4: version "3.8.0" - resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6" + resolved "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz" integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ== dependencies: buffer "^5.1.0" +cross-spawn@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz" + integrity sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A== + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -1563,88 +2396,84 @@ cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + currify@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/currify/-/currify-3.0.0.tgz#ec5b18fe65c2b3b08daba7f2a75a01063b2c89c2" + resolved "https://registry.npmjs.org/currify/-/currify-3.0.0.tgz" integrity sha512-ecz0Dq3T2UwiLwhiYvEFhdM4yUvlCLRgVbvpt6oI8RteJzEztum1UbLbN6snQ5nfHqtMcnrxkd7N0LeAIErorw== -d@1, d@^1.0.0, d@^1.0.1: +d@1, d@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + resolved "https://registry.npmjs.org/d/-/d-1.0.1.tgz" integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== dependencies: es5-ext "^0.10.50" type "^1.0.1" -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== dependencies: - assert-plus "^1.0.0" - -dayjs@^1.10.3: - version "1.10.3" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.3.tgz#cf3357c8e7f508432826371672ebf376cb7d619b" - integrity sha512-/2fdLN987N8Ki7Id8BUN2nhuiRyxTLumQnSQf9CNncFCyqFsSKb9TNhzRYcC8K8eJSJOKvbvkImo/MKKhNi4iw== + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" -debug@4, debug@^4.0.1, debug@^4.1.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" - integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== dependencies: - ms "2.1.2" + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" -debug@^2.1.3, debug@^2.6.6, debug@^2.6.8: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== dependencies: - ms "2.0.0" + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" -debug@^3.0.1, debug@^3.1.0: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" +dayjs@^1.11.8: + version "1.11.11" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e" + integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg== -debug@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== +debug@4, debug@^4.3.4, debug@^4.3.5: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== dependencies: ms "2.1.2" -debug@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -decompress-response@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" - integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= +debug@^4.1.1: + version "4.3.4" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: - mimic-response "^1.0.0" + ms "2.1.2" decompress-response@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz" integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== dependencies: mimic-response "^3.1.0" decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: version "4.1.1" - resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1" + resolved "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz" integrity sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ== dependencies: file-type "^5.2.0" @@ -1653,7 +2482,7 @@ decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: decompress-tarbz2@^4.0.0: version "4.1.1" - resolved "https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b" + resolved "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz" integrity sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A== dependencies: decompress-tar "^4.1.0" @@ -1664,7 +2493,7 @@ decompress-tarbz2@^4.0.0: decompress-targz@^4.0.0: version "4.1.1" - resolved "https://registry.yarnpkg.com/decompress-targz/-/decompress-targz-4.1.1.tgz#c09bc35c4d11f3de09f2d2da53e9de23e7ce1eee" + resolved "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz" integrity sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w== dependencies: decompress-tar "^4.1.1" @@ -1673,8 +2502,8 @@ decompress-targz@^4.0.0: decompress-unzip@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/decompress-unzip/-/decompress-unzip-4.0.1.tgz#deaaccdfd14aeaf85578f733ae8210f9b4848f69" - integrity sha1-3qrM39FK6vhVePczroIQ+bSEj2k= + resolved "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz" + integrity sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw== dependencies: file-type "^3.8.0" get-stream "^2.2.0" @@ -1683,7 +2512,7 @@ decompress-unzip@^4.0.1: decompress@^4.2.1: version "4.2.1" - resolved "https://registry.yarnpkg.com/decompress/-/decompress-4.2.1.tgz#007f55cc6a62c055afa37c07eb6a4ee1b773f118" + resolved "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz" integrity sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ== dependencies: decompress-tar "^4.0.0" @@ -1695,24 +2524,21 @@ decompress@^4.2.1: pify "^2.3.0" strip-dirs "^2.0.0" -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -defer-to-connect@^1.0.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" - integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== +defaults@^1.0.3: + version "1.0.4" + resolved "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz" + integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== + dependencies: + clone "^1.0.2" defer-to-connect@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.0.tgz#83d6b199db041593ac84d781b5222308ccf4c2c1" + resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.0.tgz" integrity sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg== deferred@^0.7.11: version "0.7.11" - resolved "https://registry.yarnpkg.com/deferred/-/deferred-0.7.11.tgz#8c3f272fd5e6ce48a969cb428c0d233ba2146322" + resolved "https://registry.npmjs.org/deferred/-/deferred-0.7.11.tgz" integrity sha512-8eluCl/Blx4YOGwMapBvXRKxHXhA8ejDXYzEaK8+/gtcm8hRMhSLmXSqDmNUKNc/C8HNSmuyyp/hflhqDAvK2A== dependencies: d "^1.0.1" @@ -1721,201 +2547,191 @@ deferred@^0.7.11: next-tick "^1.0.0" timers-ext "^0.1.7" -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== dependencies: - is-descriptor "^0.1.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= - dependencies: - is-descriptor "^1.0.0" +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== +define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" delayed-stream@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - -denque@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf" - integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ== - -detect-libc@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -diagnostics@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/diagnostics/-/diagnostics-1.1.1.tgz#cab6ac33df70c9d9a727490ae43ac995a769b22a" - integrity sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ== +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== dependencies: - colorspace "1.1.x" - enabled "1.0.x" - kuler "1.0.x" - -dijkstrajs@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.1.tgz#d3cd81221e3ea40742cfcde556d4e99e98ddc71b" - integrity sha1-082BIh4+pAdCz83lVtTpnpjdxxs= + asap "^2.0.0" + wrappy "1" dir-glob@^3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== dependencies: path-type "^4.0.0" -dot-qs@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/dot-qs/-/dot-qs-0.2.0.tgz#d36517fe24b7cda61fce7a5026a0024afaf5a439" - integrity sha1-02UX/iS3zaYfznpQJqACSvr1pDk= - -dotenv@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" - integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== - -download@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/download/-/download-8.0.0.tgz#afc0b309730811731aae9f5371c9f46be73e51b1" - integrity sha512-ASRY5QhDk7FK+XrQtQyvhpDKanLluEEQtWl/J7Lxuf/b+i8RYh997QeXvL85xitrmRKVlx9c7eTrcRdq2GS4eA== - dependencies: - archive-type "^4.0.0" - content-disposition "^0.5.2" - decompress "^4.2.1" - ext-name "^5.0.0" - file-type "^11.1.0" - filenamify "^3.0.0" - get-stream "^4.1.0" - got "^8.3.1" - make-dir "^2.1.0" - p-event "^2.1.0" - pify "^4.0.1" - -duplexer3@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" - integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= +dotenv-expand@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" + integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== -duplexify@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.1.tgz#7027dc374f157b122a8ae08c2d3ea4d2d953aa61" - integrity sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA== - dependencies: - end-of-stream "^1.4.1" - inherits "^2.0.3" - readable-stream "^3.1.1" - stream-shift "^1.0.0" +dotenv@^16.3.1: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== duration@^0.2.2: version "0.2.2" - resolved "https://registry.yarnpkg.com/duration/-/duration-0.2.2.tgz#ddf149bc3bc6901150fe9017111d016b3357f529" + resolved "https://registry.npmjs.org/duration/-/duration-0.2.2.tgz" integrity sha512-06kgtea+bGreF5eKYgI/36A6pLXggY7oR4p1pq4SmdFBn1ReOL5D8RhG64VrqfTTKNucqqtBAwEj8aB88mcqrg== dependencies: d "1" es5-ext "~0.10.46" -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== +each-parallel-async@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/each-parallel-async/-/each-parallel-async-1.0.0.tgz" + integrity sha512-P/9kLQiQj0vZNzphvKKTgRgMnlqs5cJsxeAiuog1jrUnwv0Z3hVUwJDQiP7MnLb2I9S15nR9SRUceFT9IxtqRg== emoji-regex@^8.0.0: version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -enabled@1.0.x: - version "1.0.2" - resolved "https://registry.yarnpkg.com/enabled/-/enabled-1.0.2.tgz#965f6513d2c2d1c5f4652b64a2e3396467fc2f93" - integrity sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M= - dependencies: - env-variable "0.0.x" - -end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1, end-of-stream@^1.4.4: +end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" -engine.io-client@~3.4.0: - version "3.4.4" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.4.tgz#77d8003f502b0782dd792b073a4d2cf7ca5ab967" - integrity sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ== - dependencies: - component-emitter "~1.3.0" - component-inherit "0.0.3" - debug "~3.1.0" - engine.io-parser "~2.2.0" - has-cors "1.1.0" - indexof "0.0.1" - parseqs "0.0.6" - parseuri "0.0.6" - ws "~6.1.0" - xmlhttprequest-ssl "~1.5.4" - yeast "0.1.2" - -engine.io-parser@~2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.1.tgz#57ce5611d9370ee94f99641b589f94c97e4f5da7" - integrity sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg== +eol@^0.9.1: + version "0.9.1" + resolved "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz" + integrity sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg== + +es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== + dependencies: + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" + is-callable "^1.2.7" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.3" + is-string "^1.0.7" + is-typed-array "^1.1.13" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.15" + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== dependencies: - after "0.8.2" - arraybuffer.slice "~0.0.7" - base64-arraybuffer "0.1.4" - blob "0.0.5" - has-binary2 "~1.0.2" + get-intrinsic "^1.2.4" -env-variable@0.0.x: - version "0.0.6" - resolved "https://registry.yarnpkg.com/env-variable/-/env-variable-0.0.6.tgz#74ab20b3786c545b62b4a4813ab8cf22726c9808" - integrity sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg== +es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== -error-symbol@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/error-symbol/-/error-symbol-0.1.0.tgz#0a4dae37d600d15a29ba453d8ef920f1844333f6" - integrity sha1-Ck2uN9YA0VopukU9jvkg8YRDM/Y= +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" -es5-ext@^0.10.12, es5-ext@^0.10.35, es5-ext@^0.10.45, es5-ext@^0.10.46, es5-ext@^0.10.47, es5-ext@^0.10.49, es5-ext@^0.10.50, es5-ext@^0.10.51, es5-ext@^0.10.53, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: - version "0.10.53" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" - integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q== +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== dependencies: - es6-iterator "~2.0.3" - es6-symbol "~3.1.3" - next-tick "~1.0.0" + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +es5-ext@^0.10.12, es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.47, es5-ext@^0.10.49, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@^0.10.61, es5-ext@^0.10.62, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: + version "0.10.64" + resolved "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz" + integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + esniff "^2.0.1" + next-tick "^1.1.0" -es6-iterator@^2.0.3, es6-iterator@~2.0.1, es6-iterator@~2.0.3: +es6-iterator@^2.0.3, es6-iterator@~2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + resolved "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz" integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= dependencies: d "1" @@ -1924,39 +2740,32 @@ es6-iterator@^2.0.3, es6-iterator@~2.0.1, es6-iterator@~2.0.3: es6-promisify@^6.0.0: version "6.1.1" - resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-6.1.1.tgz#46837651b7b06bf6fff893d03f29393668d01621" + resolved "https://registry.npmjs.org/es6-promisify/-/es6-promisify-6.1.1.tgz" integrity sha512-HBL8I3mIki5C1Cc9QjKUenHtnG0A5/xA8Q/AllRcfiwl2CZFXGK7ddBiCoRwAix4i2KxcQfjtIVcrVbB3vbmwg== -es6-set@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" - integrity sha1-0rPsXU2ADO2BjbU40ol02wpzzLE= - dependencies: - d "1" - es5-ext "~0.10.14" - es6-iterator "~2.0.1" - es6-symbol "3.1.1" - event-emitter "~0.3.5" - -es6-symbol@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" - integrity sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc= +es6-set@^0.1.6: + version "0.1.6" + resolved "https://registry.npmjs.org/es6-set/-/es6-set-0.1.6.tgz" + integrity sha512-TE3LgGLDIBX332jq3ypv6bcOpkLO0AslAQo7p2VqX/1N46YNsvIWgvjojjSEnWEGWMhr1qUbYeTSir5J6mFHOw== dependencies: - d "1" - es5-ext "~0.10.14" + d "^1.0.1" + es5-ext "^0.10.62" + es6-iterator "~2.0.3" + es6-symbol "^3.1.3" + event-emitter "^0.3.5" + type "^2.7.2" -es6-symbol@^3.1.1, es6-symbol@~3.1.3: +es6-symbol@^3.1.1, es6-symbol@^3.1.3: version "3.1.3" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" + resolved "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz" integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== dependencies: d "^1.0.1" ext "^1.1.2" -es6-weak-map@^2.0.2, es6-weak-map@^2.0.3: +es6-weak-map@^2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" + resolved "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz" integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== dependencies: d "1" @@ -1966,60 +2775,67 @@ es6-weak-map@^2.0.2, es6-weak-map@^2.0.3: escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== esniff@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/esniff/-/esniff-1.1.0.tgz#c66849229f91464dede2e0d40201ed6abf65f2ac" - integrity sha1-xmhJIp+RRk3t4uDUAgHtar9l8qw= + resolved "https://registry.npmjs.org/esniff/-/esniff-1.1.0.tgz" + integrity sha512-vmHXOeOt7FJLsqofvFk4WB3ejvcHizCd8toXXwADmYfd02p2QwHRgkUbhYDX54y08nqk818CUTWipgZGlyN07g== dependencies: d "1" es5-ext "^0.10.12" +esniff@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz" + integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== + dependencies: + d "^1.0.1" + es5-ext "^0.10.62" + event-emitter "^0.3.5" + type "^2.7.2" + esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -essentials@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/essentials/-/essentials-1.1.1.tgz#03befbfbee7078301741279b38a806b6ca624821" - integrity sha512-SmaxoAdVu86XkZQM/u6TYSu96ZlFGwhvSk1l9zAkznFuQkMb9mRDS2iq/XWDow7R8OwBwdYH8nLyDKznMD+GWw== +essentials@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/essentials/-/essentials-1.2.0.tgz#c6361fb648f5c8c0c51279707f6139e521a05807" + integrity sha512-kP/j7Iw7KeNE8b/o7+tr9uX2s1wegElGOoGZ2Xm35qBr4BbbEcH3/bxR2nfH9l9JANCq9AUrvKw+gRuHtZp0HQ== + dependencies: + uni-global "^1.0.0" -event-emitter@^0.3.5, event-emitter@~0.3.5: +event-emitter@^0.3.5: version "0.3.5" - resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + resolved "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz" integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk= dependencies: d "1" es5-ext "~0.10.14" +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + events@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" - integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= - -exit-on-epipe@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692" - integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw== - -expand-template@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" - integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw== ext-list@^2.0.0: version "2.2.2" - resolved "https://registry.yarnpkg.com/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37" + resolved "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz" integrity sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA== dependencies: mime-db "^1.28.0" ext-name@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/ext-name/-/ext-name-5.0.0.tgz#70781981d183ee15d13993c8822045c506c8f0a6" + resolved "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz" integrity sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ== dependencies: ext-list "^2.0.0" @@ -2027,264 +2843,242 @@ ext-name@^5.0.0: ext@^1.1.2: version "1.4.0" - resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244" + resolved "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz" integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A== dependencies: type "^2.0.0" +ext@^1.4.0, ext@^1.6.0, ext@^1.7.0: + version "1.7.0" + resolved "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + extend-shallow@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + resolved "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz" + integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== dependencies: is-extendable "^0.1.0" -extend@^3.0.0, extend@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - external-editor@^3.0.3: version "3.1.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + resolved "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz" integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== dependencies: chardet "^0.7.0" iconv-lite "^0.4.24" tmp "^0.0.33" -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= - -fast-deep-equal@^3.1.1: +fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.1.1, fast-glob@^3.2.4: - version "3.2.4" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" - integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== +fast-glob@^3.2.11: + version "3.2.12" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz" + integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.0" + glob-parent "^5.1.2" merge2 "^1.3.0" - micromatch "^4.0.2" - picomatch "^2.2.1" + micromatch "^4.0.4" -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== +fast-glob@^3.2.7, fast-glob@^3.2.9: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" -fast-safe-stringify@^2.0.4: - version "2.0.7" - resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" - integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== +fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== -fastest-levenshtein@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" - integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow== +fast-xml-parser@4.2.5, fast-xml-parser@>=4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz#86dbf3f18edf8739326447bcaac31b4ae7f6514f" + integrity sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw== + dependencies: + strnum "^1.0.5" + +fastest-levenshtein@^1.0.16: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== fastq@^1.6.0: version "1.8.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz" integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q== dependencies: reusify "^1.0.4" fd-slicer@~1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" - integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= + resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== dependencies: pend "~1.2.0" -fecha@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.0.tgz#3ffb6395453e3f3efff850404f0a59b6747f5f41" - integrity sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg== - -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" - integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= - dependencies: - escape-string-regexp "^1.0.5" - -figures@^3.0.0, figures@^3.2.0: +figures@^3.0.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + resolved "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz" integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== dependencies: escape-string-regexp "^1.0.5" -file-type@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-11.1.0.tgz#93780f3fed98b599755d846b99a1617a2ad063b8" - integrity sha512-rM0UO7Qm9K7TWTtA6AShI/t7H5BPjDeGVDaNyg9BjHAj3PysKy7+8C8D137R88jnR3rFJZQB/tFgydl5sN5m7g== - -file-type@^3.8.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" - integrity sha1-JXoHg4TR24CHvESdEH1SpSZyuek= - -file-type@^4.2.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-4.4.0.tgz#1b600e5fca1fbdc6e80c0a70c71c8dba5f7906c5" - integrity sha1-G2AOX8ofvcboDApwxxyNul95BsU= - -file-type@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6" - integrity sha1-LdvqfHP/42No365J3DOMBYwritY= - -file-type@^6.1.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919" - integrity sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg== - -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== +file-type@^16.5.4, file-type@^3.8.0, file-type@^4.2.0, file-type@^5.2.0, file-type@^6.1.0: + version "16.5.4" + resolved "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz" + integrity sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw== + dependencies: + readable-web-to-node-stream "^3.0.0" + strtok3 "^6.2.4" + token-types "^4.1.1" filename-reserved-regex@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz#abf73dfab735d045440abfea2d91f389ebbfa229" - integrity sha1-q/c9+rc10EVECr/qLZHzieu/oik= + resolved "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz" + integrity sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ== -filenamify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/filenamify/-/filenamify-3.0.0.tgz#9603eb688179f8c5d40d828626dcbb92c3a4672c" - integrity sha512-5EFZ//MsvJgXjBAFJ+Bh2YaCTRF/VP1YOmGrgt+KJ4SFRLjI87EIdwLLuT6wQX0I4F9W41xutobzczjsOKlI/g== +filenamify@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz" + integrity sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg== dependencies: filename-reserved-regex "^2.0.0" - strip-outer "^1.0.0" + strip-outer "^1.0.1" trim-repeated "^1.0.0" -filesize@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-6.1.0.tgz#e81bdaa780e2451d714d71c0d7a4f3238d37ad00" - integrity sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg== +filesize@^10.0.7: + version "10.1.2" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.1.2.tgz#33bb71c5c134102499f1bc36e6f2863137f6cb0c" + integrity sha512-Dx770ai81ohflojxhU+oG+Z2QGvKdYxgEr9OSA8UVrqhwNHjfH9A8f5NKfg83fEH8ZFA5N5llJo5T3PIoZ4CRA== -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-cache-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz" + integrity sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg== dependencies: - to-regex-range "^5.0.1" + common-path-prefix "^3.0.0" + pkg-dir "^7.0.0" find-requires@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/find-requires/-/find-requires-1.0.0.tgz#a4a750ed37133dee8a9cc8efd2cc56aca01dd96d" + resolved "https://registry.npmjs.org/find-requires/-/find-requires-1.0.0.tgz" integrity sha512-UME7hNwBfzeISSFQcBEDemEEskpOjI/shPrpJM5PI4DSdn6hX0dmz+2dL70blZER2z8tSnTRL+2rfzlYgtbBoQ== dependencies: es5-ext "^0.10.49" esniff "^1.1.0" +find-up@^6.3.0: + version "6.3.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz" + integrity sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw== + dependencies: + locate-path "^7.1.0" + path-exists "^5.0.0" + flat@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== -follow-redirects@^1.10.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7" - integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg== - -for-in@^0.1.3: - version "0.1.8" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" - integrity sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE= - -for-in@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= - -for-own@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" - integrity sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs= +folder-hash@^3.3.0: + version "3.3.3" + resolved "https://registry.npmjs.org/folder-hash/-/folder-hash-3.3.3.tgz" + integrity sha512-SDgHBgV+RCjrYs8aUwCb9rTgbTVuSdzvFmLaChsLre1yf+D64khCW++VYciaByZ8Rm0uKF8R/XEpXuTRSGUM1A== dependencies: - for-in "^1.0.1" + debug "^4.1.1" + graceful-fs "~4.2.0" + minimatch "~3.0.4" -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +follow-redirects@^1.14.7, follow-redirects@^1.15.0: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== -form-data@^2.3.1, form-data@^2.5.0: - version "2.5.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" - integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" + is-callable "^1.1.3" -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== dependencies: asynckit "^0.4.0" - combined-stream "^1.0.6" + combined-stream "^1.0.8" mime-types "^2.1.12" -formidable@^1.2.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" - integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== - -from2@^2.1.1: - version "2.3.0" - resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" - integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= +formidable@^2.0.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.2.tgz#fa973a2bec150e4ce7cac15589d7a25fc30ebd89" + integrity sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g== dependencies: - inherits "^2.0.1" - readable-stream "^2.0.0" + dezalgo "^1.0.4" + hexoid "^1.0.0" + once "^1.4.0" + qs "^6.11.0" fs-constants@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== fs-copy-file@^2.1.2: version "2.1.2" - resolved "https://registry.yarnpkg.com/fs-copy-file/-/fs-copy-file-2.1.2.tgz#a9360c8b0e34b58239a8d38a922dab539caf1ca3" + resolved "https://registry.npmjs.org/fs-copy-file/-/fs-copy-file-2.1.2.tgz" integrity sha512-h5h3i58/mr86CSJvDLGV0ZEIUj4QfdfKt0NFX6AH4sRTRjs2/d5U1EQt5C9fUV6ZSi7MeSfZRW3LX9HttLXHeg== dependencies: "@cloudcmd/copy-file" "^1.1.0" -fs-extra@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" - integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== +fs-extra@^10.1.0: + version "10.1.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== dependencies: - graceful-fs "^4.1.2" + graceful-fs "^4.2.0" jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" - integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== dependencies: at-least-node "^1.0.0" graceful-fs "^4.2.0" jsonfile "^6.0.1" - universalify "^1.0.0" + universalify "^2.0.0" fs-minipass@^2.0.0: version "2.1.0" @@ -2295,105 +3089,119 @@ fs-minipass@^2.0.0: fs.realpath@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fs2@^0.3.8: - version "0.3.8" - resolved "https://registry.yarnpkg.com/fs2/-/fs2-0.3.8.tgz#8930ac841240b7cf95f5a19e2c72824b87cbc1b0" - integrity sha512-HxOTRiFS3PqwAOmlp1mTwLA+xhQBdaP82b5aBamc/rHKFVyn4qL8YpngaAleD52PNMzBm6TsGOoU/Hq+bAfBhA== +fs2@^0.3.9: + version "0.3.9" + resolved "https://registry.npmjs.org/fs2/-/fs2-0.3.9.tgz" + integrity sha512-WsOqncODWRlkjwll+73bAxVW3JPChDgaPX3DT4iTTm73UmG4VgALa7LaFblP232/DN60itkOrPZ8kaP1feksGQ== dependencies: d "^1.0.1" deferred "^0.7.11" es5-ext "^0.10.53" event-emitter "^0.3.5" - ignore "^5.1.4" + ignore "^5.1.8" memoizee "^0.4.14" - type "^2.0.0" - -fsevents@~2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" - integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + type "^2.1.0" -fsevents@~2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.1.tgz#b209ab14c61012636c8863507edf7fb68cc54e9f" - integrity sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw== +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== function-bind@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" get-stdin@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== -get-stream@3.0.0, get-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" - integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= - get-stream@^2.2.0: version "2.3.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de" - integrity sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4= + resolved "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz" + integrity sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA== dependencies: object-assign "^4.0.1" pinkie-promise "^2.0.0" -get-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - get-stream@^5.1.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz" integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== dependencies: pump "^3.0.0" -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= - dependencies: - assert-plus "^1.0.0" +get-stream@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== -github-from-package@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" - integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== + dependencies: + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" -glob-parent@^5.1.0, glob-parent@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" -glob@^7.0.5, glob@^7.1.4: +glob@^7.0.5, glob@^7.2.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.4: version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== dependencies: fs.realpath "^1.0.0" @@ -2403,324 +3211,271 @@ glob@^7.0.5, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -globby@^11.0.2: - version "11.0.2" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.2.tgz#1af538b766a3b540ebfb58a32b2e2d5897321d83" - integrity sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og== +glob@^7.1.6: + version "7.1.7" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globalthis@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== dependencies: array-union "^2.1.0" dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" slash "^3.0.0" -got@^11.8.1: - version "11.8.1" - resolved "https://registry.yarnpkg.com/got/-/got-11.8.1.tgz#df04adfaf2e782babb3daabc79139feec2f7e85d" - integrity sha512-9aYdZL+6nHmvJwHALLwKSUZ0hMwGaJGYv3hoPLPgnT8BoBXm1SjnZeky+91tfwJaDzun2s4RsBRy48IEYv2q2Q== +globby@^13.1.3: + version "13.1.4" + resolved "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz" + integrity sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g== + dependencies: + dir-glob "^3.0.1" + fast-glob "^3.2.11" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^4.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +got@^11.8.6: + version "11.8.6" + resolved "https://registry.npmjs.org/got/-/got-11.8.6.tgz" + integrity sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g== dependencies: "@sindresorhus/is" "^4.0.0" "@szmarczak/http-timer" "^4.0.5" "@types/cacheable-request" "^6.0.1" "@types/responselike" "^1.0.0" cacheable-lookup "^5.0.3" - cacheable-request "^7.0.1" + cacheable-request "^7.0.2" decompress-response "^6.0.0" http2-wrapper "^1.0.0-beta.5.2" lowercase-keys "^2.0.0" p-cancelable "^2.0.0" responselike "^2.0.0" -got@^8.3.1: - version "8.3.2" - resolved "https://registry.yarnpkg.com/got/-/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937" - integrity sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw== - dependencies: - "@sindresorhus/is" "^0.7.0" - cacheable-request "^2.1.1" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^3.0.0" - into-stream "^3.1.0" - is-retry-allowed "^1.1.0" - isurl "^1.0.0-alpha5" - lowercase-keys "^1.0.0" - mimic-response "^1.0.0" - p-cancelable "^0.4.0" - p-timeout "^2.0.1" - pify "^3.0.0" - safe-buffer "^5.1.1" - timed-out "^4.0.1" - url-parse-lax "^3.0.0" - url-to-options "^1.0.1" - -got@^9.6.0: - version "9.6.0" - resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" - integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== - dependencies: - "@sindresorhus/is" "^0.14.0" - "@szmarczak/http-timer" "^1.1.2" - cacheable-request "^6.0.0" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^4.1.0" - lowercase-keys "^1.0.1" - mimic-response "^1.0.1" - p-cancelable "^1.0.0" - to-readable-stream "^1.0.0" - url-parse-lax "^3.0.0" - -graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: - version "4.2.4" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== - -graphlib@^2.1.7, graphlib@^2.1.8: +graceful-fs@^4.1.10, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@~4.2.0: + version "4.2.11" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +graphlib@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da" integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A== dependencies: lodash "^4.17.15" -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~5.1.3: - version "5.1.5" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== - dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" - -has-binary2@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d" - integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw== - dependencies: - isarray "2.0.1" - -has-cors@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" - integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk= +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== has-flag@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== has-flag@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbol-support-x@^1.4.1: - version "1.4.2" - resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455" - integrity sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw== - -has-to-string-tag-x@^1.2.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz#a045ab383d7b4b2012a00148ab0aa5f290044d4d" - integrity sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw== +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== dependencies: - has-symbol-support-x "^1.4.1" + es-define-property "^1.0.0" -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= +has-proto@^1.0.1, has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" has@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== dependencies: function-bind "^1.1.1" -http-cache-semantics@3.8.1: - version "3.8.1" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" - integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" -http-cache-semantics@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== +hexoid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" +http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== http2-wrapper@^1.0.0-beta.5.2: version "1.0.0-beta.5.2" - resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz#8b923deb90144aea65cf834b016a340fc98556f3" + resolved "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz" integrity sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ== dependencies: quick-lru "^5.1.1" resolve-alpn "^1.0.0" -https-proxy-agent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b" - integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg== - dependencies: - agent-base "5" - debug "4" - -https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== +https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" debug "4" -iconv-lite@^0.4.24, iconv-lite@~0.4.11: +iconv-lite@^0.4.24: version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" ieee754@1.1.13, ieee754@^1.1.4: version "1.1.13" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz" integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== -ignore@^5.1.4, ignore@^5.1.8: - version "5.1.8" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" - integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^5.1.8, ignore@^5.2.0: + version "5.2.4" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" - integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== imurmurhash@^0.1.4: version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -indexof@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" - integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== inflight@^1.0.4: version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= dependencies: once "^1.3.0" wrappy "1" -info-symbol@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/info-symbol/-/info-symbol-0.1.0.tgz#27841d72867ddb4242cd612d79c10633881c6a78" - integrity sha1-J4QdcoZ920JCzWEtecEGM4gcang= - -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: +inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@^1.3.7, ini@^1.3.8, ini@~1.3.0: - version "1.3.7" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" - integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== - -inquirer-autocomplete-prompt@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/inquirer-autocomplete-prompt/-/inquirer-autocomplete-prompt-1.3.0.tgz#fcbba926be2d3cf338e3dd24380ae7c408113b46" - integrity sha512-zvAc+A6SZdcN+earG5SsBu1RnQdtBS4o8wZ/OqJiCfL34cfOx+twVRq7wumYix6Rkdjn1N2nVCcO3wHqKqgdGg== - dependencies: - ansi-escapes "^4.3.1" - chalk "^4.0.0" - figures "^3.2.0" - run-async "^2.4.0" - rxjs "^6.6.2" - -inquirer@^6.0.0: - version "6.5.2" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" - integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== - dependencies: - ansi-escapes "^3.2.0" - chalk "^2.4.2" - cli-cursor "^2.1.0" - cli-width "^2.0.0" - external-editor "^3.0.3" - figures "^2.0.0" - lodash "^4.17.12" - mute-stream "0.0.7" - run-async "^2.2.0" - rxjs "^6.4.0" - string-width "^2.1.0" - strip-ansi "^5.1.0" - through "^2.3.6" +ini@^1.3.7: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -inquirer@^7.3.3: - version "7.3.3" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" - integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== +inquirer@^8.2.5: + version "8.2.5" + resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz" + integrity sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ== dependencies: ansi-escapes "^4.2.1" - chalk "^4.1.0" + chalk "^4.1.1" cli-cursor "^3.1.0" cli-width "^3.0.0" external-editor "^3.0.3" figures "^3.0.0" - lodash "^4.17.19" + lodash "^4.17.21" mute-stream "0.0.8" + ora "^5.4.1" run-async "^2.4.0" - rxjs "^6.6.0" + rxjs "^7.5.5" string-width "^4.1.0" strip-ansi "^6.0.0" through "^2.3.6" + wrap-ansi "^7.0.0" install@^0.13.0: version "0.13.0" - resolved "https://registry.yarnpkg.com/install/-/install-0.13.0.tgz#6af6e9da9dd0987de2ab420f78e60d9c17260776" + resolved "https://registry.npmjs.org/install/-/install-0.13.0.tgz" integrity sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA== -into-stream@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" - integrity sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY= +internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== dependencies: - from2 "^2.1.1" - p-is-promise "^1.1.0" + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= +is-arguments@^1.0.4: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== dependencies: - kind-of "^3.0.2" + call-bind "^1.0.2" + has-tostringtag "^1.0.0" -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== +is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== dependencies: - kind-of "^6.0.0" + call-bind "^1.0.2" + get-intrinsic "^1.2.1" -is-arrayish@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" - integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" is-binary-path@~2.1.0: version "2.1.0" @@ -2729,118 +3484,105 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== dependencies: - kind-of "^3.0.2" + call-bind "^1.0.2" + has-tostringtag "^1.0.0" -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" + is-typed-array "^1.1.13" -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-docker@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-1.1.0.tgz#f04374d4eee5310e9a8e113bf1495411e46176a1" - integrity sha1-8EN01O7lMQ6ajhE78UlUEeRhdqE= + has-tostringtag "^1.0.0" -is-docker@^2.0.0, is-docker@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.1.1.tgz#4125a88e44e450d384e09047ede71adc2d144156" - integrity sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw== +is-docker@^2.0.0, is-docker@^2.1.1, is-docker@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== -is-extendable@^0.1.0, is-extendable@^0.1.1: +is-extendable@^0.1.0: version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + resolved "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz" + integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== is-extglob@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - is-fullwidth-code-point@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-glob@^4.0.1, is-glob@~4.0.1: +is-generator-function@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + +is-glob@^4.0.0, is-glob@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz" integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== dependencies: is-extglob "^2.1.1" +is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + is-natural-number@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" - integrity sha1-q5124dtM7VHjXeDHLr7PCfc0zeg= + resolved "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz" + integrity sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ== -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= - dependencies: - kind-of "^3.0.2" +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== -is-number@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-6.0.0.tgz#e6d15ad31fc262887cccf217ae5f9316f81b1995" - integrity sha512-Wu1VHeILBK8KAWJUAiSZQX94GmOE45Rg6/538fKwiloUu21KncEkYGPqob2oSZ5mUT73vLGrHQjKw3KMPwfDzg== +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" is-number@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-object@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470" - integrity sha1-iVJojF7C/9awPsyF52ngKQMINHA= - is-plain-obj@^1.0.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" - integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz" + integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== is-plain-object@^2.0.4: version "2.0.4" @@ -2849,105 +3591,117 @@ is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-promise@^2.1, is-promise@^2.2.2: +is-primitive@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-3.0.1.tgz#98c4db1abff185485a657fc2905052b940524d05" + integrity sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w== + +is-promise@^2.2.2: version "2.2.2" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" + resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz" integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== -is-retry-allowed@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" - integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== + dependencies: + call-bind "^1.0.7" is-stream@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" + integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== -is-typedarray@^1.0.0, is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.13, is-typed-array@^1.1.3: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" is-windows@^1.0.1: version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + resolved "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== -is-wsl@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" - integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= - is-wsl@^2.1.1, is-wsl@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== dependencies: is-docker "^2.0.0" is@^3.2.1: version "3.3.0" - resolved "https://registry.yarnpkg.com/is/-/is-3.3.0.tgz#61cff6dd3c4193db94a3d62582072b44e5645d79" + resolved "https://registry.npmjs.org/is/-/is-3.3.0.tgz" integrity sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg== -is_js@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/is_js/-/is_js-0.9.0.tgz#0ab94540502ba7afa24c856aa985561669e9c52d" - integrity sha1-CrlFQFArp6+iTIVqqYVWFmnpxS0= - -isarray@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" - integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4= - isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== isexe@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isobject@^3.0.0, isobject@^3.0.1: +isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== isomorphic-ws@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= - -isurl@^1.0.0-alpha5: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67" - integrity sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w== - dependencies: - has-to-string-tag-x "^1.2.0" - is-object "^1.0.1" - -jmespath@0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" - integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= - -js-yaml@^3.13.1, js-yaml@^3.14.0: - version "3.14.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" - integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" +jmespath@0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" + integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== -js-yaml@^3.14.1: +js-yaml@^3.13.1, js-yaml@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -2955,32 +3709,30 @@ js-yaml@^3.14.1: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f" - integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q== +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: argparse "^2.0.1" -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - -json-buffer@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" - integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= - json-buffer@3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== -json-cycle@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/json-cycle/-/json-cycle-1.3.0.tgz#c4f6f7d926c2979012cba173b06f9cae9e866d3f" - integrity sha512-FD/SedD78LCdSvJaOUQAXseT8oQBb5z6IVYaQaCrVUlu9zOAr1BDdKyVYQaSD/GDsAMrXpKcOyBD4LIl8nfjHw== +json-colorizer@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/json-colorizer/-/json-colorizer-2.2.2.tgz#07c2ac8cef36558075948e1566c6cfb4ac1668e6" + integrity sha512-56oZtwV1piXrQnRNTtJeqRv+B9Y/dXAYLqBBaYl/COcUdoZxgLBLAO88+CnkbT6MxNs0c5E9mPBIb2sFcNz3vw== + dependencies: + chalk "^2.4.1" + lodash.get "^4.4.2" + +json-cycle@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/json-cycle/-/json-cycle-1.5.0.tgz#b1f1d976eee16cef51d5f3d3b3caece3e90ba23a" + integrity sha512-GOehvd5PO2FeZ5T4c+RxobeT5a1PiGpF4u9/3+UvrMU4bhnVqzJY7hm39wg8PDCqkU91fWGH8qjWR4bn+wgq9w== json-refs@^3.0.15: version "3.0.15" @@ -2996,143 +3748,62 @@ json-refs@^3.0.15: slash "^3.0.0" uri-js "^4.2.2" -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= +json-schema@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== jsonfile@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz" integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= optionalDependencies: graceful-fs "^4.1.6" jsonfile@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179" + resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz" integrity sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg== dependencies: universalify "^1.0.0" optionalDependencies: graceful-fs "^4.1.6" -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" - -jszip@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.5.0.tgz#b4fd1f368245346658e781fec9675802489e15f6" - integrity sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA== +jszip@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" + integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== dependencies: lie "~3.3.0" pako "~1.0.2" readable-stream "~2.3.6" - set-immediate-shim "~1.0.1" + setimmediate "^1.0.5" jwt-decode@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79" - integrity sha1-fYa9VmefWM5qhHBKZX3TkruoGnk= + integrity sha512-86GgN2vzfUu7m9Wcj63iUkuDzFNYFVmjeDm2GzWpUk+opB0pEpMsw6ePCMrhYkumz2C1ihqtZzOMAg7FiXcNoQ== -kafka-node@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/kafka-node/-/kafka-node-5.0.0.tgz#4b6f65cc1d77ebe565859dfb8f9575ed15d543c0" - integrity sha512-dD2ga5gLcQhsq1yNoQdy1MU4x4z7YnXM5bcG9SdQuiNr5KKuAmXixH1Mggwdah5o7EfholFbcNDPSVA6BIfaug== - dependencies: - async "^2.6.2" - binary "~0.3.0" - bl "^2.2.0" - buffer-crc32 "~0.2.5" - buffermaker "~1.2.0" - debug "^2.1.3" - denque "^1.3.0" - lodash "^4.17.4" - minimatch "^3.0.2" - nested-error-stacks "^2.0.0" - optional "^0.1.3" - retry "^0.10.1" - uuid "^3.0.0" - optionalDependencies: - snappy "^6.0.1" - -keyv@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373" - integrity sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA== - dependencies: - json-buffer "3.0.0" - -keyv@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" - integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== - dependencies: - json-buffer "3.0.0" +jwt-decode@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz" + integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== keyv@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.3.tgz#4f3aa98de254803cafcd2896734108daa35e4254" - integrity sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA== + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== dependencies: json-buffer "3.0.1" -kind-of@^3.0.2, kind-of@^3.0.3: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0, kind-of@^5.0.2: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -koalas@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/koalas/-/koalas-1.0.2.tgz#318433f074235db78fae5661a02a8ca53ee295cd" - integrity sha1-MYQz8HQjXbePrlZhoCqMpT7ilc0= - -kuler@1.0.x: - version "1.0.1" - resolved "https://registry.yarnpkg.com/kuler/-/kuler-1.0.1.tgz#ef7c784f36c9fb6e16dd3150d152677b2b0228a6" - integrity sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ== - dependencies: - colornames "^1.1.1" - -lazy-cache@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264" - integrity sha1-uRkKT5EzVGlIQIWfio9whNiCImQ= - dependencies: - set-getter "^0.1.0" - lazystream@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" + resolved "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz" integrity sha1-9plf4PggOS9hOWvolGJAe7dxaOQ= dependencies: readable-stream "^2.0.5" @@ -3144,158 +3815,132 @@ lie@~3.3.0: dependencies: immediate "~3.0.5" +locate-path@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz" + integrity sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA== + dependencies: + p-locate "^6.0.0" + +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash.defaults@^4.2.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + resolved "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz" integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= lodash.difference@^4.5.0: version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" + resolved "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz" integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw= lodash.flatten@^4.4.0: version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + resolved "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz" integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + lodash.isplainobject@^4.0.6: version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz" integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= lodash.union@^4.6.0: version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" + resolved "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz" integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg= -lodash@4.17.x, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lodash@4.17.21, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-ok@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/log-ok/-/log-ok-0.1.1.tgz#bea3dd36acd0b8a7240d78736b5b97c65444a334" - integrity sha1-vqPdNqzQuKckDXhza1uXxlREozQ= +log-node@^8.0.3: + version "8.0.3" + resolved "https://registry.npmjs.org/log-node/-/log-node-8.0.3.tgz" + integrity sha512-1UBwzgYiCIDFs8A0rM2QdBFo8Wd8UQ0HrSTu/MNI+/2zN3NoHRj2fhplurAyuxTYUXu3Oohugq1jAn5s05u1MQ== dependencies: - ansi-green "^0.1.1" - success-symbol "^0.1.0" + ansi-regex "^5.0.1" + cli-color "^2.0.1" + cli-sprintf-format "^1.1.1" + d "^1.0.1" + es5-ext "^0.10.53" + sprintf-kit "^2.0.1" + supports-color "^8.1.1" + type "^2.5.0" -log-utils@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/log-utils/-/log-utils-0.2.1.tgz#a4c217a0dd9a50515d9b920206091ab3d4e031cf" - integrity sha1-pMIXoN2aUFFdm5ICBgkas9TgMc8= - dependencies: - ansi-colors "^0.2.0" - error-symbol "^0.1.0" - info-symbol "^0.1.0" - log-ok "^0.1.1" - success-symbol "^0.1.0" - time-stamp "^1.0.1" - warning-symbol "^0.1.0" - -log@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/log/-/log-6.0.0.tgz#1e8e655f0389148e729d9ddd6d3bcbe8b93b8d21" - integrity sha512-sxChESNYJ/EcQv8C7xpmxhtTOngoXuMEqGDAkhXBEmt3MAzM3SM/TmIBOqnMEVdrOv1+VgZoYbo6U2GemQiU4g== +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== dependencies: - d "^1.0.0" - duration "^0.2.2" - es5-ext "^0.10.49" - event-emitter "^0.3.5" - sprintf-kit "^2.0.0" - type "^1.0.1" + chalk "^4.1.0" + is-unicode-supported "^0.1.0" -logform@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2" - integrity sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg== +log@^6.0.0, log@^6.3.1: + version "6.3.1" + resolved "https://registry.npmjs.org/log/-/log-6.3.1.tgz" + integrity sha512-McG47rJEWOkXTDioZzQNydAVvZNeEkSyLJ1VWkFwfW+o1knW+QSi8D1KjPn/TnctV+q99lkvJNe1f0E1IjfY2A== dependencies: - colors "^1.2.1" - fast-safe-stringify "^2.0.4" - fecha "^4.2.0" - ms "^2.1.1" - triple-beam "^1.3.0" - -long@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/long/-/long-1.1.2.tgz#eaef5951ca7551d96926b82da242db9d6b28fb53" - integrity sha1-6u9ZUcp1UdlpJrgtokLbnWso+1M= - -long@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" - integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== - -lowercase-keys@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" - integrity sha1-TjNms55/VFfjXxMkvfb4jQv8cwY= - -lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" - integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + d "^1.0.1" + duration "^0.2.2" + es5-ext "^0.10.53" + event-emitter "^0.3.5" + sprintf-kit "^2.0.1" + type "^2.5.0" + uni-global "^1.0.0" lowercase-keys@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== +lru-cache@^4.0.1: + version "4.1.5" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== dependencies: - yallist "^4.0.0" + pseudomap "^1.0.2" + yallist "^2.1.2" -lru-queue@0.1, lru-queue@^0.1.0: +lru-queue@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" + resolved "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz" integrity sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM= dependencies: es5-ext "~0.10.2" make-dir@^1.0.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz" integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== dependencies: pify "^3.0.0" -make-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== - dependencies: - pify "^4.0.1" - semver "^5.6.0" - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= +make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== dependencies: - object-visit "^1.0.0" + semver "^6.0.0" -memoizee@^0.4.14: - version "0.4.14" - resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.14.tgz#07a00f204699f9a95c2d9e77218271c7cd610d57" - integrity sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg== +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== dependencies: - d "1" - es5-ext "^0.10.45" - es6-weak-map "^2.0.2" - event-emitter "^0.3.5" - is-promise "^2.1" - lru-queue "0.1" - next-tick "1" - timers-ext "^0.1.5" + semver "^7.5.3" -memoizee@^0.4.15: +memoizee@^0.4.14, memoizee@^0.4.15: version "0.4.15" - resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.15.tgz#e6f3d2da863f318d02225391829a6c5956555b72" + resolved "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz" integrity sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ== dependencies: d "^1.0.1" @@ -3307,85 +3952,107 @@ memoizee@^0.4.15: next-tick "^1.1.0" timers-ext "^0.1.7" -merge2@^1.3.0: +merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -methods@^1.1.1: +methods@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== +micromatch@^4.0.4: + version "4.0.4" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== dependencies: braces "^3.0.1" - picomatch "^2.0.5" + picomatch "^2.2.3" -mime-db@1.44.0: - version "1.44.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== +micromatch@^4.0.5: + version "4.0.7" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" + integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" -mime-db@^1.28.0: - version "1.45.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" - integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== +mime-db@1.52.0, mime-db@^1.28.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.27" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: - mime-db "1.44.0" + mime-db "1.52.0" -mime@^1.2.11, mime@^1.4.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== -mimic-fn@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" - integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== mimic-fn@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mimic-response@^1.0.0, mimic-response@^1.0.1: +mimic-response@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== mimic-response@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz" integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== -minimatch@^3.0.2, minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== +minimatch@^3.0.2, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^3.0.4, minimatch@~3.0.4: + version "3.0.8" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz" + integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q== dependencies: brace-expansion "^1.1.7" -minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minimatch@^5.0.1, minimatch@^5.1.0: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== minipass@^3.0.0: - version "3.1.3" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" - integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== dependencies: yallist "^4.0.0" +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + minizlib@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -3394,570 +4061,370 @@ minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" -mixin-object@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" - integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4= - dependencies: - for-in "^0.1.3" - is-extendable "^0.1.1" - -mkdirp@^0.5.1: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== +mkdirp@^0.5.6: + version "0.5.6" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== dependencies: - minimist "^1.2.5" + minimist "^1.2.6" mkdirp@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -moment@^2.29.1: - version "2.29.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" - integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.2, ms@^2.1.1: +ms@2.1.2: version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -mute-stream@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" - integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== mute-stream@0.0.8: version "0.0.8" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.14.1: - version "2.14.2" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" - integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== - -nanoid@^2.1.0: - version "2.1.11" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280" - integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA== - -napi-build-utils@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" - integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== - native-promise-only@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11" - integrity sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE= + integrity sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg== -ncjsm@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ncjsm/-/ncjsm-4.1.0.tgz#4af4a57d560211cca9783ea875f361cb801f108d" - integrity sha512-YElRGtbz5iIartetOI3we+XAkcGE29F0SdNC0qRy500/u4WceQd2z9Nhlx24OHmIDIKz9MHdJwf/fkSG0hdWcQ== +ncjsm@^4.3.2: + version "4.3.2" + resolved "https://registry.npmjs.org/ncjsm/-/ncjsm-4.3.2.tgz" + integrity sha512-6d1VWA7FY31CpI4Ki97Fpm36jfURkVbpktizp8aoVViTZRQgr/0ddmlKerALSSlzfwQRBeSq1qwwVcBJK4Sk7Q== dependencies: - builtin-modules "^3.1.0" + builtin-modules "^3.3.0" deferred "^0.7.11" - es5-ext "^0.10.53" - es6-set "^0.1.5" + es5-ext "^0.10.62" + es6-set "^0.1.6" + ext "^1.7.0" find-requires "^1.0.0" - fs2 "^0.3.8" - type "^2.0.0" - -nested-error-stacks@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61" - integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug== + fs2 "^0.3.9" + type "^2.7.2" next-tick@1, next-tick@^1.0.0, next-tick@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + resolved "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz" integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== -next-tick@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" - integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= - nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -node-abi@^2.7.0: - version "2.19.1" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.19.1.tgz#6aa32561d0a5e2fdb6810d8c25641b657a8cea85" - integrity sha512-HbtmIuByq44yhAzK7b9j/FelKlHYISKQn0mtvcBrU5QBkhoCMp5bu8Hv5AI34DcKfOAcJBcOEMwLlwO62FFu9A== +nmtree@^1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/nmtree/-/nmtree-1.0.6.tgz" + integrity sha512-SUPCoyX5w/lOT6wD/PZEymR+J899984tYEOYjuDqQlIOeX5NSb1MEsCcT0az+dhZD0MLAj5hGBZEpKQxuDdniA== dependencies: - semver "^5.4.1" + commander "^2.11.0" node-dir@^0.1.17: version "0.1.17" resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5" - integrity sha1-X1Zl2TNRM1yqvvjxxVRRbPXx5OU= + integrity sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg== dependencies: minimatch "^3.0.2" -node-fetch@^2.6.0, node-fetch@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== +node-fetch@^2.6.11, node-fetch@^2.6.7, node-fetch@^2.6.8: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-fetch@^2.6.9: + version "2.6.11" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz" + integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w== + dependencies: + whatwg-url "^5.0.0" node.extend@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/node.extend/-/node.extend-2.0.2.tgz#b4404525494acc99740f3703c496b7d5182cc6cc" + resolved "https://registry.npmjs.org/node.extend/-/node.extend-2.0.2.tgz" integrity sha512-pDT4Dchl94/+kkgdwyS2PauDFjZG0Hk0IcHIB+LkW27HLDtdoeMxHTxZh39DYbPP8UflWXWj9JcdDozF+YDOpQ== dependencies: has "^1.0.3" is "^3.2.1" -noop-logger@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" - integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI= - normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -normalize-url@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" - integrity sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw== - dependencies: - prepend-http "^2.0.0" - query-string "^5.0.1" - sort-keys "^2.0.0" - -normalize-url@^4.1.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" - integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== +normalize-url@^4.5.1, normalize-url@^6.0.1: + version "4.5.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" + integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== -npmlog@^4.0.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== +npm-registry-utilities@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/npm-registry-utilities/-/npm-registry-utilities-1.0.0.tgz#75dc21fcb96020d506b99823407c2088508a4edd" + integrity sha512-9xYfSJy2IFQw1i6462EJzjChL9e65EfSo2Cw6kl0EFeDp05VvU+anrQk3Fc0d1MbVCq7rWIxeer89O9SUQ/uOg== dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + ext "^1.6.0" + fs2 "^0.3.9" + memoizee "^0.4.15" + node-fetch "^2.6.7" + semver "^7.3.5" + type "^2.6.0" + validate-npm-package-name "^3.0.0" -object-assign@^4.0.1, object-assign@^4.1.0: +object-assign@^4.0.1: version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== -object-hash@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.1.1.tgz#9447d0279b4fcf80cff3259bf66a1dc73afabe09" - integrity sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ== +object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== dependencies: - isobject "^3.0.0" + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.1" once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: wrappy "1" -one-time@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/one-time/-/one-time-0.0.4.tgz#f8cdf77884826fe4dff93e3a9cc37b1e4480742e" - integrity sha1-+M33eISCb+Tf+T46nMN7HkSAdC4= - -onetime@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" - integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= - dependencies: - mimic-fn "^1.0.0" - onetime@^5.1.0: version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" -open@^7.3.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/open/-/open-7.3.0.tgz#45461fdee46444f3645b6e14eb3ca94b82e1be69" - integrity sha512-mgLwQIx2F/ye9SmbrUkurZCnkoXyXyu9EbHtJZrICjVAJfyMArdHp3KkixGdZx1ZHFPNIwl0DDM1dFFqXbTLZw== +open@^7.4.2: + version "7.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" + integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== dependencies: is-docker "^2.0.0" is-wsl "^2.1.1" -open@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/open/-/open-7.3.1.tgz#111119cb919ca1acd988f49685c4fdd0f4755356" - integrity sha512-f2wt9DCBKKjlFbjzGb8MOAW8LH8F0mrs1zc7KTjAJ9PZNQbfenzWbNP1VZJvw6ICMG9r14Ah6yfwPn7T7i646A== +open@^8.4.2: + version "8.4.2" + resolved "https://registry.npmjs.org/open/-/open-8.4.2.tgz" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== dependencies: - is-docker "^2.0.0" - is-wsl "^2.1.1" + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" -opn@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" - integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== +ora@^5.4.1: + version "5.4.1" + resolved "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== dependencies: - is-wsl "^1.1.0" - -optional@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/optional/-/optional-0.1.4.tgz#cdb1a9bedc737d2025f690ceeb50e049444fd5b3" - integrity sha512-gtvrrCfkE08wKcgXaVwQVgwEQ8vel2dc5DDBn9RLQZ3YtmtkBss6A2HY6BnJH4N/4Ku97Ri/SF8sNWE2225WJw== - -os-homedir@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" os-tmpdir@~1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -p-cancelable@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" - integrity sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ== - -p-cancelable@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" - integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== + resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== p-cancelable@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e" + resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz" integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg== -p-event@^2.1.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/p-event/-/p-event-2.3.1.tgz#596279ef169ab2c3e0cae88c1cfbb08079993ef6" - integrity sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA== +p-event@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz" + integrity sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ== dependencies: - p-timeout "^2.0.1" + p-timeout "^3.1.0" p-finally@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= - -p-is-promise@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" - integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4= + resolved "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== -p-limit@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== +p-limit@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz" + integrity sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ== dependencies: - yocto-queue "^0.1.0" + yocto-queue "^1.0.0" -p-timeout@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038" - integrity sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA== +p-locate@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz" + integrity sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw== dependencies: - p-finally "^1.0.0" + p-limit "^4.0.0" -package-json@^6.3.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" - integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== +p-timeout@^3.1.0: + version "3.2.0" + resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz" + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== dependencies: - got "^9.6.0" - registry-auth-token "^4.0.0" - registry-url "^5.0.0" - semver "^6.2.0" + p-finally "^1.0.0" pako@~1.0.2: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== -parseqs@0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5" - integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w== - -parseuri@0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a" - integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow== +path-exists@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz" + integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ== path-is-absolute@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-loader@^1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/path-loader/-/path-loader-1.0.10.tgz#dd3d1bd54cb6f2e6423af2ad334a41cc0bce4cf6" - integrity sha512-CMP0v6S6z8PHeJ6NFVyVJm6WyJjIwFvyz2b0n2/4bKdS/0uZa/9sKUlYZzubrn3zuDRU0zIuEDX9DZYQ2ZI8TA== + version "1.0.12" + resolved "https://registry.yarnpkg.com/path-loader/-/path-loader-1.0.12.tgz#c5a99d464da27cfde5891d158a68807abbdfa5f5" + integrity sha512-n7oDG8B+k/p818uweWrOixY9/Dsr89o2TkCm6tOTex3fpdo2+BFDgR+KpB37mGKBRsBAlR8CIJMFN0OEy/7hIQ== dependencies: native-promise-only "^0.8.1" - superagent "^3.8.3" + superagent "^7.1.6" path-type@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +path2@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/path2/-/path2-0.1.0.tgz#639828942cdbda44a41a45b074ae8873483b4efa" + integrity sha512-TX+cz8Jk+ta7IvRy2FAej8rdlbrP0+uBIkP/5DTODez/AuL/vSb30KuAdDxGVREXzn8QfAiu5mJYJ1XjbOhEPA== + +peek-readable@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz" + integrity sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg== + pend@~1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" - integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== -picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== pify@^2.3.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== pify@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + resolved "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz" + integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== pinkie-promise@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + resolved "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz" + integrity sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw== dependencies: pinkie "^2.0.0" pinkie@^2.0.0: version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + resolved "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" + integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== pipe-io@^3.0.0: version "3.0.12" - resolved "https://registry.yarnpkg.com/pipe-io/-/pipe-io-3.0.12.tgz#90ff84888876a1feccbf9f753eacf22b260b2884" + resolved "https://registry.npmjs.org/pipe-io/-/pipe-io-3.0.12.tgz" integrity sha512-reR49NtpkVgedzCQ9DPV727VAZKw8Ax3N/3iQwD1vHxTmswsuhurFh0Z5woVNM1OhHDigKzDN7u4kNipAA9yyA== -pointer-symbol@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/pointer-symbol/-/pointer-symbol-1.0.0.tgz#60f9110204ea7a929b62644a21315543cbb3d447" - integrity sha1-YPkRAgTqepKbYmRKITFVQ8uz1Ec= - -prebuild-install@5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.0.tgz#58b4d8344e03590990931ee088dd5401b03004c8" - integrity sha512-aaLVANlj4HgZweKttFNUVNRxDukytuIuxeK2boIMHjagNJCiVKWFsKF4tCE3ql3GbrD2tExPQ7/pwtEJcHNZeg== - dependencies: - detect-libc "^1.0.3" - expand-template "^2.0.3" - github-from-package "0.0.0" - minimist "^1.2.0" - mkdirp "^0.5.1" - napi-build-utils "^1.0.1" - node-abi "^2.7.0" - noop-logger "^0.1.1" - npmlog "^4.0.1" - os-homedir "^1.0.1" - pump "^2.0.1" - rc "^1.2.7" - simple-get "^2.7.0" - tar-fs "^1.13.0" - tunnel-agent "^0.6.0" - which-pm-runs "^1.0.0" - -prepend-http@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" - integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= - -prettyoutput@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/prettyoutput/-/prettyoutput-1.2.0.tgz#fef93f2a79c032880cddfb84308e2137e3674b22" - integrity sha512-G2gJwLzLcYS+2m6bTAe+CcDpwak9YpcvpScI0tE4WYb2O3lEZD/YywkMNpGqsSx5wttGvh2UXaKROTKKCyM2dw== +pkg-dir@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz" + integrity sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA== dependencies: - colors "1.3.x" - commander "2.19.x" - lodash "4.17.x" + find-up "^6.3.0" -printj@~1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" - integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ== +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== process-nextick-args@~2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process-utils@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/process-utils/-/process-utils-4.0.0.tgz" + integrity sha512-fMyMQbKCxX51YxR7YGCzPjLsU3yDzXFkP4oi1/Mt5Ixnk7GO/7uUTj8mrCHUwuvozWzI+V7QSJR9cZYnwNOZPg== + dependencies: + ext "^1.4.0" + fs2 "^0.3.9" + memoizee "^0.4.14" + type "^2.1.0" + promise-queue@^2.2.5: version "2.2.5" resolved "https://registry.yarnpkg.com/promise-queue/-/promise-queue-2.2.5.tgz#2f6f5f7c0f6d08109e967659c79b88a9ed5e93b4" - integrity sha1-L29ffA9tCBCelnZZx5uIqe1ek7Q= - -prompt-actions@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/prompt-actions/-/prompt-actions-3.0.2.tgz#537eee52241c940379f354a06eae8528e44ceeba" - integrity sha512-dhz2Fl7vK+LPpmnQ/S/eSut4BnH4NZDLyddHKi5uTU/2PDn3grEMGkgsll16V5RpVUh/yxdiam0xsM0RD4xvtg== - dependencies: - debug "^2.6.8" - -prompt-base@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/prompt-base/-/prompt-base-4.1.0.tgz#7b88e4c01b096c83d2f4e501a7e85f0d369ecd1f" - integrity sha512-svGzgLUKZoqomz9SGMkf1hBG8Wl3K7JGuRCXc/Pv7xw8239hhaTBXrmjt7EXA9P/QZzdyT8uNWt9F/iJTXq75g== - dependencies: - component-emitter "^1.2.1" - debug "^3.0.1" - koalas "^1.0.2" - log-utils "^0.2.1" - prompt-actions "^3.0.2" - prompt-question "^5.0.1" - readline-ui "^2.2.3" - readline-utils "^2.2.3" - static-extend "^0.1.2" - -prompt-choices@^4.0.5: - version "4.1.0" - resolved "https://registry.yarnpkg.com/prompt-choices/-/prompt-choices-4.1.0.tgz#6094202c4e55d0762e49c1e53735727e53fd484f" - integrity sha512-ZNYLv6rW9z9n0WdwCkEuS+w5nUAGzRgtRt6GQ5aFNFz6MIcU7nHFlHOwZtzy7RQBk80KzUGPSRQphvMiQzB8pg== - dependencies: - arr-flatten "^1.1.0" - arr-swap "^1.0.1" - choices-separator "^2.0.0" - clone-deep "^4.0.0" - collection-visit "^1.0.0" - define-property "^2.0.2" - is-number "^6.0.0" - kind-of "^6.0.2" - koalas "^1.0.2" - log-utils "^0.2.1" - pointer-symbol "^1.0.0" - radio-symbol "^2.0.0" - set-value "^3.0.0" - strip-color "^0.1.0" - terminal-paginator "^2.0.2" - toggle-array "^1.0.1" - -prompt-confirm@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/prompt-confirm/-/prompt-confirm-1.2.0.tgz#ed96d0ecc3a3485c7c9d7103bf19444e7811631f" - integrity sha512-r7XZxI5J5/oPtUskN0ZYO+lkv/WJHMQgfd1GTKAuxnHuViQShiFHdUnj6DamL4gQExaKAX7rnIcTKoRSpVVquA== - dependencies: - debug "^2.6.8" - prompt-base "^4.0.1" - -prompt-question@^5.0.1: - version "5.0.2" - resolved "https://registry.yarnpkg.com/prompt-question/-/prompt-question-5.0.2.tgz#81a479f38f0bafecc758e5d6f7bc586e599610b3" - integrity sha512-wreaLbbu8f5+7zXds199uiT11Ojp59Z4iBi6hONlSLtsKGTvL2UY8VglcxQ3t/X4qWIxsNCg6aT4O8keO65v6Q== - dependencies: - clone-deep "^1.0.0" - debug "^3.0.1" - define-property "^1.0.0" - isobject "^3.0.1" - kind-of "^5.0.2" - koalas "^1.0.2" - prompt-choices "^4.0.5" - -protobufjs@^6.9.0: - version "6.10.1" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.10.1.tgz#e6a484dd8f04b29629e9053344e3970cccf13cd2" - integrity sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ== - dependencies: - "@protobufjs/aspromise" "^1.1.2" - "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" - "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" - "@protobufjs/path" "^1.1.2" - "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" - "@types/long" "^4.0.1" - "@types/node" "^13.7.0" - long "^4.0.0" - -psl@^1.1.28: - version "1.8.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" - integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + integrity sha512-p/iXrPSVfnqPft24ZdNNLECw/UrtLTpT3jpAAMzl/o5/rDsGCPo3/CQS2611flL6LkoEJ3oQZw7C8Q80ZISXRQ== -pump@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954" - integrity sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== -pump@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" - integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz" + integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== pump@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz" integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== dependencies: end-of-stream "^1.1.0" @@ -3966,84 +4433,43 @@ pump@^3.0.0: punycode@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw== -punycode@^2.1.0, punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -qrcode-terminal@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819" - integrity sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ== - -qs@^6.5.1: - version "6.9.4" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" - integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== - -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -query-string@^5.0.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" - integrity sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw== +qs@^6.10.3, qs@^6.11.0: + version "6.12.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a" + integrity sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ== dependencies: - decode-uri-component "^0.2.0" - object-assign "^4.1.0" - strict-uri-encode "^1.0.0" + side-channel "^1.0.6" -querystring@0.2.0, querystring@^0.2.0: +querystring@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== + +querystring@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd" + integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== quick-lru@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -radio-symbol@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/radio-symbol/-/radio-symbol-2.0.0.tgz#7aa9bfc50485636d52dd76d6a8e631b290799ae1" - integrity sha1-eqm/xQSFY21S3XbWqOYxspB5muE= - dependencies: - ansi-gray "^0.1.1" - ansi-green "^0.1.1" - is-windows "^1.0.1" - -ramda@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.25.0.tgz#8fdf68231cffa90bc2f9460390a0cb74a29b29a9" - integrity sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ== - -ramda@^0.26.1: - version "0.26.1" - resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" - integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== - -ramda@^0.27.1: - version "0.27.1" - resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.1.tgz#66fc2df3ef873874ffc2da6aa8984658abacf5c9" - integrity sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw== - -rc@^1.2.7, rc@^1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@^2.3.7, readable-stream@~2.3.6: +readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.3.6: version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== dependencies: core-util-is "~1.0.0" @@ -4054,499 +4480,412 @@ readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.0.6, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.0, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== +readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" util-deprecate "^1.0.1" -readdir-glob@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.1.tgz#f0e10bb7bf7bfa7e0add8baffdc54c3f7dbee6c4" - integrity sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA== +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== dependencies: - minimatch "^3.0.4" + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" -readdirp@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== +readable-web-to-node-stream@^3.0.0: + version "3.0.2" + resolved "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz" + integrity sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw== dependencies: - picomatch "^2.2.1" + readable-stream "^3.6.0" -readline-ui@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/readline-ui/-/readline-ui-2.2.3.tgz#9e873a7668bbd8ca8a5573ce810a6bafb70a5089" - integrity sha512-ix7jz0PxqQqcIuq3yQTHv1TOhlD2IHO74aNO+lSuXsRYm1d+pdyup1yF3zKyLK1wWZrVNGjkzw5tUegO2IDy+A== +readdir-glob@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584" + integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA== dependencies: - component-emitter "^1.2.1" - debug "^2.6.8" - readline-utils "^2.2.1" - string-width "^2.0.0" + minimatch "^5.1.0" -readline-utils@^2.2.1, readline-utils@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/readline-utils/-/readline-utils-2.2.3.tgz#6f847d6b8f1915c391b581c367cd47873862351a" - integrity sha1-b4R9a48ZFcORtYHDZ81HhzhiNRo= +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== dependencies: - arr-flatten "^1.1.0" - extend-shallow "^2.0.1" - is-buffer "^1.1.5" - is-number "^3.0.0" - is-windows "^1.0.1" - koalas "^1.0.2" - mute-stream "0.0.7" - strip-color "^0.1.0" - window-size "^1.1.0" + picomatch "^2.2.1" -regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7: +regenerator-runtime@^0.13.4: version "0.13.7" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz" integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== -registry-auth-token@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.0.tgz#1d37dffda72bbecd0f581e4715540213a65eb7da" - integrity sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w== - dependencies: - rc "^1.2.8" - -registry-url@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" - integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== +regexp.prototype.flags@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== dependencies: - rc "^1.2.8" - -replaceall@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/replaceall/-/replaceall-0.1.6.tgz#81d81ac7aeb72d7f5c4942adf2697a3220688d8e" - integrity sha1-gdgax663LX9cSUKt8ml6MiBojY4= + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" -request-promise-core@1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f" - integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw== - dependencies: - lodash "^4.17.19" - -request-promise-native@^1.0.8: - version "1.0.9" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28" - integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g== - dependencies: - request-promise-core "1.1.4" - stealthy-require "^1.1.1" - tough-cookie "^2.3.3" - -request@^2.88.0: - version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== resolve-alpn@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.0.0.tgz#745ad60b3d6aff4b4a48e01b8c0bdc70959e0e8c" + resolved "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.0.0.tgz" integrity sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA== -responselike@1.0.2, responselike@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" - integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= - dependencies: - lowercase-keys "^1.0.0" - responselike@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723" + resolved "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz" integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw== dependencies: lowercase-keys "^2.0.0" -restore-cursor@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" - integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= - dependencies: - onetime "^2.0.0" - signal-exit "^3.0.2" - restore-cursor@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz" integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== dependencies: onetime "^5.1.0" signal-exit "^3.0.2" -retry@^0.10.1: - version "0.10.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" - integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q= - reusify@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -run-async@^2.2.0, run-async@^2.4.0: +run-async@^2.4.0: version "2.4.1" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz" integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== -run-parallel-limit@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/run-parallel-limit/-/run-parallel-limit-1.0.6.tgz#0982a893d825b050cbaff1a35414832b195541b6" - integrity sha512-yFFs4Q2kECi5mWXyyZj3UlAZ5OFq5E07opABC+EmhZdjEkrxXaUwFqOaaNF4tbayMnBxrsbujpeCYTVjGufZGQ== +run-parallel-limit@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/run-parallel-limit/-/run-parallel-limit-1.1.0.tgz#be80e936f5768623a38a963262d6bef8ff11e7ba" + integrity sha512-jJA7irRNM91jaKc3Hcl1npHsFLOXOoTkPCUL1JEa1R82O2miplXXRaGdjW/KM/98YQWDhJLiSs793CnXfblJUw== + dependencies: + queue-microtask "^1.2.2" run-parallel@^1.1.9: version "1.1.9" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" + resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz" integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== -rxjs@^6.4.0, rxjs@^6.6.0, rxjs@^6.6.2: - version "6.6.3" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" - integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== +rxjs@^7.5.5: + version "7.8.0" + resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz" + integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== dependencies: - tslib "^1.9.0" + tslib "^2.1.0" + +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + has-symbols "^1.0.3" + isarray "^2.0.5" + +safe-buffer@5.2.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-regex "^1.1.4" -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3": version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sax@1.2.1: +sax@1.2.1, sax@>=0.6.0: version "1.2.1" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" - integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= - -sax@>=0.6.0: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + integrity sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA== seek-bzip@^1.0.5: version "1.0.6" - resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.6.tgz#35c4171f55a680916b52a07859ecf3b5857f21c4" + resolved "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz" integrity sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ== dependencies: commander "^2.8.1" -semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +semver@^5.5.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@^6.1.1, semver@^6.2.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^6.0.0: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.4: - version "7.3.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" - integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== - dependencies: - lru-cache "^6.0.0" +semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== -serverless-finch@^2.3.2: - version "2.6.0" - resolved "https://registry.yarnpkg.com/serverless-finch/-/serverless-finch-2.6.0.tgz#c74e7492dbfae52aa6383d4a21bac9138bcd9383" - integrity sha512-G5umIBoNyo3MKCtdtbbkkb/7Z84qNstbQnkdscG/VhukYUib+7BiWidAMI+WAFq+JEUf3PW7c3bvt/uFEiMnnA== +serverless-finch@^4.0.3: + version "4.0.4" + resolved "https://registry.npmjs.org/serverless-finch/-/serverless-finch-4.0.4.tgz" + integrity sha512-jpZmtM/ggtccMOA27OkQL0CkCMDfK7xALl4Zl/hBiysyKh562Xya3V7eukbTf4tZOJvBlC6+AGpsxMOaCBn4tQ== dependencies: - is_js "^0.9.0" - mime "^1.2.11" - minimatch "^3.0.4" - prompt-confirm "^1.2.0" + "@serverless/utils" "^6.0.2" + mime "^3.0.0" + minimatch "^5.0.1" -serverless-layers@^1.4.3: - version "1.5.0" - resolved "https://registry.yarnpkg.com/serverless-layers/-/serverless-layers-1.5.0.tgz#f1596c7f65f9ef76d061e0d0f8b908c32b50c94e" - integrity sha512-/VnGeEVoaE8w23lgMRw5W3CMlf/7tLq35px+Ab0QyvCK+WnNKX5VtHUzDyIBA/WrFXw7XXWeNKnqLezsuoiLyw== +serverless-layers@^2.6.1: + version "2.6.1" + resolved "https://registry.npmjs.org/serverless-layers/-/serverless-layers-2.6.1.tgz" + integrity sha512-jE7SO1//SHJbm/KiZd2WzZXrhGUxAki3AmubQqq5C1fMe61lHMy2om+QlvIccGZ0+MUuLIWhDcFiwW25ncH97w== dependencies: "@babel/runtime" "^7.3.1" archiver "^3.0.0" bluebird "^3.5.3" + chalk "^3.0.0" + folder-hash "^3.3.0" fs-copy-file "^2.1.2" - mkdirp "^0.5.1" + fs-extra "^8.1.0" + glob "^7.1.6" + mkdirp "^0.5.6" + semver "^7.3.2" + slugify "^1.4.0" serverless-plugin-tracing@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/serverless-plugin-tracing/-/serverless-plugin-tracing-2.0.0.tgz#df6b8b3166ac9bb70a37c7fc875014b2369158f6" + resolved "https://registry.npmjs.org/serverless-plugin-tracing/-/serverless-plugin-tracing-2.0.0.tgz" integrity sha1-32uLMWasm7cKN8f8h1AUsjaRWPY= -serverless-prune-plugin@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/serverless-prune-plugin/-/serverless-prune-plugin-1.4.3.tgz#556d76a86e37bf57d4ccd8449a7d98b6496bd5ed" - integrity sha512-gsZF3oLs5rFdp6ynjiWf5cuXZ4DZrAhxRd5Zf2gfH/43kPqtZMZzUqcGYbHh1OXbOzogdn8fEg5d4Q3xxWwRBA== +serverless-prune-plugin@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/serverless-prune-plugin/-/serverless-prune-plugin-2.0.2.tgz" + integrity sha512-tW1Q8MAVmhW8KQN+e0AsSVsb9nmRWWj28xBjMwvVC3FbammmtUJT+5nRpmjxJZ6/K/j3OV1Rx8b32md71BwkYQ== dependencies: - bluebird "^3.4.7" + bluebird "^3.7.2" -serverless-pseudo-parameters@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/serverless-pseudo-parameters/-/serverless-pseudo-parameters-2.5.0.tgz#f30bf34db166e4b8b22144a8e65aca71b90dd1e6" - integrity sha512-A/O49AR8LL6jlnPSmnOTYgL1KqVgskeRla4sVDeS/r5dHFJlwOU5MgFilc7aaQP8NWAwRJANaIS9oiSE3I+VUA== - -serverless@^2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/serverless/-/serverless-2.19.0.tgz#c464f0abac97f1a2da9d009cac17541e8b78050d" - integrity sha512-JvNB+llJXIfsMk6weTh5/aCMEvTGnizQ/ZHfyyXhLuBHm0cAa9h6bpyBagnC5CTtV++jwCR2WKu2a0SQQEmEvA== - dependencies: - "@serverless/cli" "^1.5.2" - "@serverless/components" "^3.4.7" - "@serverless/enterprise-plugin" "^4.4.2" - "@serverless/utils" "^2.2.0" - ajv "^6.12.6" - ajv-keywords "^3.5.2" - archiver "^5.2.0" - aws-sdk "^2.828.0" +serverless@^3.32.2: + version "3.39.0" + resolved "https://registry.yarnpkg.com/serverless/-/serverless-3.39.0.tgz#699fbea4d0b0ba0baba0510be743f9d025ac363d" + integrity sha512-FHI3fhe4TRS8+ez/KA7HmO3lt3fAynO+N1pCCzYRThMWG0J8RWCI0BI+K0mw9+sEV+QpBCpZRZbuGyUaTsaQew== + dependencies: + "@aws-sdk/client-api-gateway" "^3.588.0" + "@aws-sdk/client-cognito-identity-provider" "^3.588.0" + "@aws-sdk/client-eventbridge" "^3.588.0" + "@aws-sdk/client-iam" "^3.588.0" + "@aws-sdk/client-lambda" "^3.588.0" + "@aws-sdk/client-s3" "^3.588.0" + "@serverless/dashboard-plugin" "^7.2.0" + "@serverless/platform-client" "^4.5.1" + "@serverless/utils" "^6.13.1" + abort-controller "^3.0.0" + ajv "^8.12.0" + ajv-formats "^2.1.1" + archiver "^5.3.1" + aws-sdk "^2.1404.0" bluebird "^3.7.2" - boxen "^5.0.0" cachedir "^2.3.0" - chalk "^4.1.0" + chalk "^4.1.2" child-process-ext "^2.1.1" + ci-info "^3.8.0" + cli-progress-footer "^2.3.2" d "^1.0.1" - dayjs "^1.10.3" + dayjs "^1.11.8" decompress "^4.2.1" - dotenv "^8.2.0" - download "^8.0.0" - essentials "^1.1.1" - fastest-levenshtein "^1.0.12" - filesize "^6.1.0" - fs-extra "^9.0.1" + dotenv "^16.3.1" + dotenv-expand "^10.0.0" + essentials "^1.2.0" + ext "^1.7.0" + fastest-levenshtein "^1.0.16" + filesize "^10.0.7" + fs-extra "^10.1.0" get-stdin "^8.0.0" - globby "^11.0.2" - got "^11.8.1" - graceful-fs "^4.2.4" - https-proxy-agent "^5.0.0" - is-docker "^2.1.1" - is-wsl "^2.2.0" - js-yaml "^4.0.0" - json-cycle "^1.3.0" + globby "^11.1.0" + graceful-fs "^4.2.11" + https-proxy-agent "^5.0.1" + is-docker "^2.2.1" + js-yaml "^4.1.0" + json-colorizer "^2.2.2" + json-cycle "^1.5.0" json-refs "^3.0.15" - lodash "^4.17.20" + lodash "^4.17.21" memoizee "^0.4.15" - micromatch "^4.0.2" - ncjsm "^4.1.0" - node-fetch "^2.6.1" - object-hash "^2.1.1" - p-limit "^3.1.0" + micromatch "^4.0.5" + node-fetch "^2.6.11" + npm-registry-utilities "^1.0.0" + object-hash "^3.0.0" + open "^8.4.2" + path2 "^0.1.0" + process-utils "^4.0.0" promise-queue "^2.2.5" - replaceall "^0.1.6" - semver "^7.3.4" - tabtab "^3.0.2" - tar "^6.1.0" + require-from-string "^2.0.2" + semver "^7.5.3" + signal-exit "^3.0.7" + stream-buffers "^3.0.2" + strip-ansi "^6.0.1" + supports-color "^8.1.1" + tar "^6.1.15" timers-ext "^0.1.7" - type "^2.1.0" + type "^2.7.2" untildify "^4.0.0" - uuid "^8.3.2" + uuid "^9.0.0" + ws "^7.5.9" yaml-ast-parser "0.0.43" - yargs-parser "^20.2.4" - -set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= -set-getter@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376" - integrity sha1-12nBgsnVpR9AkUXy+6guXoboA3Y= +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== dependencies: - to-object-path "^0.3.0" - -set-immediate-shim@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" - integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E= + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" -set-value@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-3.0.2.tgz#74e8ecd023c33d0f77199d415409a40f21e61b90" - integrity sha512-npjkVoz+ank0zjlV9F47Fdbjfj/PfXyVhZvGALWsyIYU/qrMzpi6avjKW3/7KeSU2Df3I46BrN1xOI1+6vW0hA== +set-function-name@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== dependencies: - is-plain-object "^2.0.4" + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" -shallow-clone@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571" - integrity sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA== +set-value@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-4.1.0.tgz#aa433662d87081b75ad88a4743bd450f044e7d09" + integrity sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw== dependencies: - is-extendable "^0.1.1" - kind-of "^5.0.0" - mixin-object "^2.0.1" + is-plain-object "^2.0.4" + is-primitive "^3.0.1" -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== shebang-command@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz" + integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg== dependencies: shebang-regex "^1.0.0" -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -shortid@^2.2.14: - version "2.2.15" - resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.15.tgz#2b902eaa93a69b11120373cd42a1f1fe4437c122" - integrity sha512-5EaCy2mx2Jgc/Fdn9uuDuNIIfWBpzY4XIlhoqtXF6qsf+/+SGZ+FxDdX/ZsMZiWupIWNqAEmiNY4RC+LSmCeOw== +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: - nanoid "^2.1.0" + shebang-regex "^3.0.0" -signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz" + integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== -simple-concat@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" - integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -simple-get@^2.7.0: - version "2.8.1" - resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-2.8.1.tgz#0e22e91d4575d87620620bc91308d57a77f44b5d" - integrity sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw== +side-channel@^1.0.4, side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== dependencies: - decompress-response "^3.3.0" - once "^1.3.1" - simple-concat "^1.0.0" + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^3.0.2, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -simple-git@^2.31.0: - version "2.31.0" - resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-2.31.0.tgz#3e5954c1e36c76fb382c08eaa2749a206db9f613" - integrity sha512-/+rmE7dYZMbRAfEmn8EUIOwlM2G7UdzpkC60KF86YAfXGnmGtsPrKsym0hKvLBdFLLW019C+aZld1+6iIVy5xA== +simple-git@^3.16.0: + version "3.25.0" + resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-3.25.0.tgz#3666e76d6831f0583dc380645945b97e0ac4aab6" + integrity sha512-KIY5sBnzc4yEcJXW7Tdv4viEz8KyG+nU0hay+DWZasvdFOYKeUZ6Xc25LUHHjw0tinPT7O1eY6pzX7pRT1K8rw== dependencies: "@kwsites/file-exists" "^1.1.1" "@kwsites/promise-deferred" "^1.1.1" - debug "^4.3.1" - -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" - integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= - dependencies: - is-arrayish "^0.3.1" + debug "^4.3.5" slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -snappy@^6.0.1: - version "6.3.5" - resolved "https://registry.yarnpkg.com/snappy/-/snappy-6.3.5.tgz#c14b8dea8e9bc2687875b5e491d15dd900e6023c" - integrity sha512-lonrUtdp1b1uDn1dbwgQbBsb5BbaiLeKq+AGwOk2No+en+VvJThwmtztwulEQsLinRF681pBqib0NUZaizKLIA== - dependencies: - bindings "^1.3.1" - nan "^2.14.1" - prebuild-install "5.3.0" +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== -socket.io-client@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.1.tgz#91a4038ef4d03c19967bb3c646fec6e0eaa78cff" - integrity sha512-YXmXn3pA8abPOY//JtYxou95Ihvzmg8U6kQyolArkIyLd0pgVhrfor/iMsox8cn07WCOOvvuJ6XKegzIucPutQ== - dependencies: - backo2 "1.0.2" - component-bind "1.0.0" - component-emitter "~1.3.0" - debug "~3.1.0" - engine.io-client "~3.4.0" - has-binary2 "~1.0.2" - indexof "0.0.1" - parseqs "0.0.6" - parseuri "0.0.6" - socket.io-parser "~3.3.0" - to-array "0.1.4" - -socket.io-parser@~3.3.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.1.tgz#f07d9c8cb3fb92633aa93e76d98fd3a334623199" - integrity sha512-1QLvVAe8dTz+mKmZ07Swxt+LAo4Y1ff50rlyoEx00TQmDFVQYPfcqGvIDJLGaBdhdNCecXtyKpD+EgKGcmmbuQ== - dependencies: - component-emitter "~1.3.0" - debug "~3.1.0" - isarray "2.0.1" +slugify@^1.4.0: + version "1.6.0" + resolved "https://registry.npmjs.org/slugify/-/slugify-1.6.0.tgz" + integrity sha512-FkMq+MQc5hzYgM86nLuHI98Acwi3p4wX+a5BO9Hhw4JdK4L7WueIiZ4tXEobImPqBz2sVcV0+Mu3GRB30IGang== sort-keys-length@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/sort-keys-length/-/sort-keys-length-1.0.1.tgz#9cb6f4f4e9e48155a6aa0671edd336ff1479a188" - integrity sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg= + resolved "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz" + integrity sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw== dependencies: sort-keys "^1.0.0" sort-keys@^1.0.0: version "1.1.2" - resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" - integrity sha1-RBttTTRnmPG05J6JIK37oOVD+a0= - dependencies: - is-plain-obj "^1.0.0" - -sort-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" - integrity sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg= + resolved "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz" + integrity sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg== dependencies: is-plain-obj "^1.0.0" -source-map-support@^0.5.19: - version "0.5.19" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +sort-object-keys@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-1.1.3.tgz" + integrity sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg== -split2@^3.1.1: +split2@^3.1.1, split2@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== @@ -4556,47 +4895,19 @@ split2@^3.1.1: sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -sprintf-kit@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/sprintf-kit/-/sprintf-kit-2.0.0.tgz#47499d636e9cc68f2f921d30eb4f0b911a2d7835" - integrity sha512-/0d2YTn8ZFVpIPAU230S9ZLF8WDkSSRWvh/UOLM7zzvkCchum1TtouRgyV8OfgOaYilSGU4lSSqzwBXJVlAwUw== - dependencies: - es5-ext "^0.10.46" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -sshpk@^1.7.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - -stack-trace@0.0.x: - version "0.0.10" - resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" - integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= - -static-extend@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= +sprintf-kit@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/sprintf-kit/-/sprintf-kit-2.0.1.tgz" + integrity sha512-2PNlcs3j5JflQKcg4wpdqpZ+AjhQJ2OZEo34NXDtlB0tIPG84xaaXhpA8XFacFiwjKA4m49UOYG83y3hbMn/gQ== dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" + es5-ext "^0.10.53" -stealthy-require@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" - integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= +stream-buffers@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-3.0.2.tgz#5249005a8d5c2d00b3a32e6e0a6ea209dc4f3521" + integrity sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ== stream-promise@^3.2.0: version "3.2.0" @@ -4607,137 +4918,107 @@ stream-promise@^3.2.0: es5-ext "^0.10.49" is-stream "^1.1.0" -stream-shift@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" - integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== - -strict-uri-encode@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" - integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= - -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= +string-width@^4.1.0: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" -string-width@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" -string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" string_decoder@^1.1.1: version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: safe-buffer "~5.2.0" string_decoder@~1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== dependencies: safe-buffer "~5.1.0" -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: - ansi-regex "^5.0.0" - -strip-color@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/strip-color/-/strip-color-0.1.0.tgz#106f65d3d3e6a2d9401cac0eb0ce8b8a702b4f7b" - integrity sha1-EG9l09PmotlAHKwOsM6LinArT3s= + ansi-regex "^5.0.1" strip-dirs@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5" + resolved "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz" integrity sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g== dependencies: is-natural-number "^4.0.1" -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -strip-outer@^1.0.0: +strip-outer@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-1.0.1.tgz#b2fd2abf6604b9d1e6013057195df836b8a9d631" + resolved "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz" integrity sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg== dependencies: escape-string-regexp "^1.0.2" -success-symbol@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/success-symbol/-/success-symbol-0.1.0.tgz#24022e486f3bf1cdca094283b769c472d3b72897" - integrity sha1-JAIuSG878c3KCUKDt2nEctO3KJc= - -superagent@^3.8.3: - version "3.8.3" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128" - integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA== - dependencies: - component-emitter "^1.2.0" - cookiejar "^2.1.0" - debug "^3.1.0" - extend "^3.0.0" - form-data "^2.3.1" - formidable "^1.2.0" - methods "^1.1.1" - mime "^1.4.1" - qs "^6.5.1" - readable-stream "^2.3.5" +strnum@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" + integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== + +strtok3@^6.2.4: + version "6.3.0" + resolved "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz" + integrity sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw== + dependencies: + "@tokenizer/token" "^0.3.0" + peek-readable "^4.1.0" + +superagent@^7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-7.1.6.tgz#64f303ed4e4aba1e9da319f134107a54cacdc9c6" + integrity sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.3" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.0.1" + methods "^1.1.2" + mime "2.6.0" + qs "^6.10.3" + readable-stream "^3.6.0" + semver "^7.3.7" supports-color@^5.3.0: version "5.5.0" @@ -4746,38 +5027,45 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + supports-color@^7.1.0: version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" -tabtab@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/tabtab/-/tabtab-3.0.2.tgz#a2cea0f1035f88d145d7da77eaabbd3fe03e1ec9" - integrity sha512-jANKmUe0sIQc/zTALTBy186PoM/k6aPrh3A7p6AaAfF6WPSbTx1JYeGIGH162btpH+mmVEXln+UxwViZHO2Jhg== +supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== dependencies: - debug "^4.0.1" - es6-promisify "^6.0.0" - inquirer "^6.0.0" - minimist "^1.2.0" - mkdirp "^0.5.1" - untildify "^3.0.3" - -tar-fs@^1.13.0: - version "1.16.3" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.3.tgz#966a628841da2c4010406a82167cbd5e0c72d509" - integrity sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw== - dependencies: - chownr "^1.0.1" - mkdirp "^0.5.1" - pump "^1.0.0" - tar-stream "^1.1.2" - -tar-stream@^1.1.2, tar-stream@^1.5.2: + has-flag "^4.0.0" + +synp@^1.9.10: + version "1.9.10" + resolved "https://registry.npmjs.org/synp/-/synp-1.9.10.tgz" + integrity sha512-G9Z/TXTaBG1xNslUf3dHFidz/8tvvRaR560WWyOwyI7XrGGEGBTEIIg4hdRh1qFtz8mPYynAUYwWXUg/Zh0Pzw== + dependencies: + "@yarnpkg/lockfile" "^1.1.0" + bash-glob "^2.0.0" + colors "1.4.0" + commander "^7.2.0" + eol "^0.9.1" + lodash "4.17.21" + nmtree "^1.0.6" + semver "^7.3.5" + sort-object-keys "^1.1.3" + +tar-stream@^1.5.2: version "1.6.2" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" + resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz" integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A== dependencies: bl "^1.0.0" @@ -4788,9 +5076,9 @@ tar-stream@^1.1.2, tar-stream@^1.5.2: to-buffer "^1.1.1" xtend "^4.0.0" -tar-stream@^2.1.0, tar-stream@^2.1.4: +tar-stream@^2.1.0: version "2.1.4" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.4.tgz#c4fb1a11eb0da29b893a5b25476397ba2d053bfa" + resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.4.tgz" integrity sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw== dependencies: bl "^4.0.3" @@ -4799,32 +5087,29 @@ tar-stream@^2.1.0, tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" -tar@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83" - integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA== +tar-stream@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +tar@^6.1.15: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== dependencies: chownr "^2.0.0" fs-minipass "^2.0.0" - minipass "^3.0.0" + minipass "^5.0.0" minizlib "^2.1.1" mkdirp "^1.0.3" yallist "^4.0.0" -terminal-paginator@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/terminal-paginator/-/terminal-paginator-2.0.2.tgz#967e66056f28fe8f55ba7c1eebfb7c3ef371c1d3" - integrity sha512-IZMT5ECF9p4s+sNCV8uvZSW9E1+9zy9Ji9xz2oee8Jfo7hUFpauyjxkhfRcIH6Lu3Wdepv5D1kVRc8Hx74/LfQ== - dependencies: - debug "^2.6.6" - extend-shallow "^2.0.1" - log-utils "^0.2.1" - -text-hex@1.0.x: - version "1.0.0" - resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" - integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== - throat@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" @@ -4832,22 +5117,12 @@ throat@^5.0.0: through@^2.3.6, through@^2.3.8: version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - -time-stamp@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" - integrity sha1-dkpaEa9QVhkhsTPztE5hhofg9cM= - -timed-out@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" - integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= + resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== -timers-ext@^0.1.5, timers-ext@^0.1.7: +timers-ext@^0.1.7: version "0.1.7" - resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.7.tgz#6f57ad8578e07a3fb9f91d9387d65647555e25c6" + resolved "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz" integrity sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ== dependencies: es5-ext "~0.10.46" @@ -4855,277 +5130,300 @@ timers-ext@^0.1.5, timers-ext@^0.1.7: tmp@^0.0.33: version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz" integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== dependencies: os-tmpdir "~1.0.2" -to-array@0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" - integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA= - to-buffer@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" + resolved "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz" integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg== -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= - dependencies: - kind-of "^3.0.2" - -to-readable-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" - integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== - to-regex-range@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== dependencies: is-number "^7.0.0" -toggle-array@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/toggle-array/-/toggle-array-1.0.1.tgz#cbf5840792bd5097f33117ae824c932affe87d58" - integrity sha1-y/WEB5K9UJfzMReugkyTKv/ofVg= - dependencies: - isobject "^3.0.0" - -tough-cookie@^2.3.3, tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== +token-types@^4.1.1: + version "4.2.1" + resolved "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz" + integrity sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ== dependencies: - psl "^1.1.28" - punycode "^2.1.1" + "@tokenizer/token" "^0.3.0" + ieee754 "^1.2.1" -"traverse@>=0.3.0 <0.4": - version "0.3.9" - resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" - integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk= +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== traverse@^0.6.6: - version "0.6.6" - resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" - integrity sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc= + version "0.6.9" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.9.tgz#76cfdbacf06382d460b76f8b735a44a6209d8b81" + integrity sha512-7bBrcF+/LQzSgFmT0X5YclVqQxtv7TDJ1f8Wj7ibBu/U6BMLeOpUxuZjV7rMc44UtKxlnMFigdhFAIszSX1DMg== + dependencies: + gopd "^1.0.1" + typedarray.prototype.slice "^1.0.3" + which-typed-array "^1.1.15" trim-repeated@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" - integrity sha1-42RqLqTokTEr9+rObPsFOAvAHCE= + resolved "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz" + integrity sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg== dependencies: escape-string-regexp "^1.0.2" -triple-beam@^1.2.0, triple-beam@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" - integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== - -tslib@^1.9.0: +tslib@^1.11.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= +tslib@^2.1.0, tslib@^2.5.0: + version "2.5.0" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== -type-fest@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" - integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== +tslib@^2.3.1, tslib@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== type@^1.0.1: version "1.2.0" - resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" + resolved "https://registry.npmjs.org/type/-/type-1.2.0.tgz" integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== -type@^2.0.0, type@^2.1.0: +type@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/type/-/type-2.1.0.tgz#9bdc22c648cf8cf86dd23d32336a41cfb6475e3f" + resolved "https://registry.npmjs.org/type/-/type-2.1.0.tgz" integrity sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA== -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== +type@^2.1.0, type@^2.5.0, type@^2.6.0, type@^2.7.2: + version "2.7.2" + resolved "https://registry.npmjs.org/type/-/type-2.7.2.tgz" + integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== + +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" + +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + +typedarray.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typedarray.prototype.slice/-/typedarray.prototype.slice-1.0.3.tgz#bce2f685d3279f543239e4d595e0d021731d2d1a" + integrity sha512-8WbVAQAUlENo1q3c3zZYuy5k9VzBQvp8AX9WOtbvyWlLM1v5JaSRmjubLjzHF4JFtptjH/5c/i95yaElvcjC0A== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-errors "^1.3.0" + typed-array-buffer "^1.0.2" + typed-array-byte-offset "^1.0.2" + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== dependencies: - is-typedarray "^1.0.0" + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" unbzip2-stream@^1.0.9: version "1.4.3" - resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + resolved "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz" integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== dependencies: buffer "^5.2.1" through "^2.3.8" +uni-global@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/uni-global/-/uni-global-1.0.0.tgz" + integrity sha512-WWM3HP+siTxzIWPNUg7hZ4XO8clKi6NoCAJJWnuRL+BAqyFXF8gC03WNyTefGoUXYc47uYgXxpKLIEvo65PEHw== + dependencies: + type "^2.5.0" + universalify@^0.1.0: version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== universalify@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" + resolved "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz" integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== -untildify@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9" - integrity sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA== +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== untildify@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== -uri-js@^4.2.2: - version "4.4.0" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" - integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== +uri-js@^4.2.2, uri-js@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" -url-parse-lax@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" - integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= - dependencies: - prepend-http "^2.0.0" - -url-to-options@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" - integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k= - url@0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" - integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= + integrity sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ== dependencies: punycode "1.3.2" querystring "0.2.0" -urlencode@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/urlencode/-/urlencode-1.1.0.tgz#1f2ba26f013c85f0133f7a3ad6ff2730adf7cbb7" - integrity sha1-HyuibwE8hfATP3o61v8nMK33y7c= - dependencies: - iconv-lite "~0.4.11" - util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -uuid@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" - integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== +util@^0.12.4: + version "0.12.5" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" + integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + is-typed-array "^1.1.3" + which-typed-array "^1.1.2" -uuid@^3.0.0, uuid@^3.3.2, uuid@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" + integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== uuid@^8.3.2: version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -warning-symbol@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/warning-symbol/-/warning-symbol-0.1.0.tgz#bb31dd11b7a0f9d67ab2ed95f457b65825bbad21" - integrity sha1-uzHdEbeg+dZ6su2V9Fe2WCW7rSE= - -which-pm-runs@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" - integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= +uuid@^9.0.0, uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== -which@^1.2.9: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== +validate-npm-package-name@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e" + integrity sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw== dependencies: - isexe "^2.0.0" + builtins "^1.0.3" -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz" + integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== dependencies: - string-width "^1.0.2 || 2" + defaults "^1.0.3" -widest-line@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" - integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== - dependencies: - string-width "^4.0.0" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== -window-size@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-1.1.1.tgz#9858586580ada78ab26ecd6978a6e03115c1af20" - integrity sha512-5D/9vujkmVQ7pSmc0SCBmHXbkv6eaHwXEx65MywhmUMsI8sGqJ972APq1lotfcwMKPFLuCFfL8xGHLIp7jaBmA== +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== dependencies: - define-property "^1.0.0" - is-number "^3.0.0" + tr46 "~0.0.3" + webidl-conversions "^3.0.0" -winston-transport@^4.3.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59" - integrity sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw== +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.2: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + +which@^1.2.9: + version "1.3.1" + resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== dependencies: - readable-stream "^2.3.7" - triple-beam "^1.2.0" + isexe "^2.0.0" -winston@3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/winston/-/winston-3.2.1.tgz#63061377976c73584028be2490a1846055f77f07" - integrity sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw== +which@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: - async "^2.6.1" - diagnostics "^1.1.1" - is-stream "^1.1.0" - logform "^2.1.1" - one-time "0.0.4" - readable-stream "^3.1.1" - stack-trace "0.0.x" - triple-beam "^1.3.0" - winston-transport "^4.3.0" + isexe "^2.0.0" wrap-ansi@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: ansi-styles "^4.0.0" @@ -5134,75 +5432,63 @@ wrap-ansi@^7.0.0: wrappy@1: version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= wraptile@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/wraptile/-/wraptile-2.0.0.tgz#fc893b8c3b10113ce219234ee6f17b5b48654c8a" + resolved "https://registry.npmjs.org/wraptile/-/wraptile-2.0.0.tgz" integrity sha512-Jzt4wTT0DJGucp4VewhbT6YutpOfBh6Ab4r5hKWTvFYsNTCxPi0U8wOsesDk1CQ+VcHyaP36BzCiKRJTROJiTQ== -write-file-atomic@^2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" - integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== - dependencies: - graceful-fs "^4.1.11" - imurmurhash "^0.1.4" - signal-exit "^3.0.2" - -write-file-atomic@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== +write-file-atomic@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== dependencies: imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" - -ws@<7.0.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" - integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== - dependencies: - async-limiter "~1.0.0" + signal-exit "^3.0.7" -ws@^7.2.1, ws@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" - integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA== +ws@>=7.5.10, ws@^7.5.3, ws@^7.5.9: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== -ws@~6.1.0: - version "6.1.4" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9" - integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA== +xml2js@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499" + integrity sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA== dependencies: - async-limiter "~1.0.0" + sax ">=0.6.0" + xmlbuilder "~11.0.0" -xml2js@0.4.19: - version "0.4.19" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" - integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== +xml2js@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz" + integrity sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w== dependencies: sax ">=0.6.0" - xmlbuilder "~9.0.1" + xmlbuilder "~11.0.0" -xmlbuilder@~9.0.1: - version "9.0.7" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" - integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== -xmlhttprequest-ssl@~1.5.4: - version "1.5.5" - resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" - integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4= +xmlhttprequest-ssl@^1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz#03b713873b01659dfa2c1c5d056065b27ddc2de6" + integrity sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q== xtend@^4.0.0: version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz" + integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" @@ -5221,32 +5507,46 @@ yamljs@^0.3.0: argparse "^1.0.7" glob "^7.0.5" -yargs-parser@^20.2.4: - version "20.2.4" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" - integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== +yarn-audit-fix@^9.3.10: + version "9.3.10" + resolved "https://registry.npmjs.org/yarn-audit-fix/-/yarn-audit-fix-9.3.10.tgz" + integrity sha512-q4MeQuPTRNORLlxRwOJAdMOdMlqsgmbsym3SkNvD6kklMOVRWqZRlZyAlmmUepNgBaFOYI2NQCejgRz2VEIkAg== + dependencies: + "@types/find-cache-dir" "^3.2.1" + "@types/fs-extra" "^11.0.1" + "@types/lodash-es" "^4.17.6" + "@types/semver" "^7.3.13" + "@types/yarnpkg__lockfile" "^1.1.5" + "@yarnpkg/lockfile" "^1.1.0" + chalk "^5.2.0" + commander "^10.0.0" + find-cache-dir "^4.0.0" + find-up "^6.3.0" + fs-extra "^10.1.0" + globby "^13.1.3" + js-yaml "^4.1.0" + lodash-es "^4.17.21" + pkg-dir "^7.0.0" + semver "^7.3.8" + synp "^1.9.10" + tslib "^2.5.0" yauzl@^2.4.2: version "2.10.0" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" - integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= + resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== dependencies: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" -yeast@0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" - integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +yocto-queue@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz" + integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== zames@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/zames/-/zames-2.0.1.tgz#f52633e193699b707672e32aeb6d51a09b6c8b36" + resolved "https://registry.npmjs.org/zames/-/zames-2.0.1.tgz" integrity sha512-gJJxR12zrhOBl96d/9PorsFAEU+xUOtxOwO2lUofj8a40ahx+nxjQftzD35/GdxLzlJ5vTWh4oG81TpmKh/+hw== dependencies: currify "^3.0.0" @@ -5254,27 +5554,18 @@ zames@^2.0.0: zip-stream@^2.1.2: version "2.1.3" - resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-2.1.3.tgz#26cc4bdb93641a8590dd07112e1f77af1758865b" + resolved "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz" integrity sha512-EkXc2JGcKhO5N5aZ7TmuNo45budRaFGHOmz24wtJR7znbNqDPmdZtUauKX6et8KAVseAMBOyWJqEpXcHTBsh7Q== dependencies: archiver-utils "^2.1.0" compress-commons "^2.1.1" readable-stream "^3.4.0" -zip-stream@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.0.2.tgz#3a20f1bd7729c2b59fd4efa04df5eb7a5a217d2e" - integrity sha512-TGxB2g+1ur6MHkvM644DuZr8Uzyz0k0OYWtS3YlpfWBEmK4woaC2t3+pozEL3dBfIPmpgmClR5B2QRcMgGt22g== - dependencies: - archiver-utils "^2.1.0" - compress-commons "^4.0.0" - readable-stream "^3.6.0" - -zip-stream@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.0.4.tgz#3a8f100b73afaa7d1ae9338d910b321dec77ff3a" - integrity sha512-a65wQ3h5gcQ/nQGWV1mSZCEzCML6EK/vyVPcrPNynySP1j3VBbQKh3nhC8CbORb+jfl2vXvh56Ul5odP1bAHqw== +zip-stream@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.1.tgz#1337fe974dbaffd2fa9a1ba09662a66932bd7135" + integrity sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ== dependencies: - archiver-utils "^2.1.0" - compress-commons "^4.0.2" + archiver-utils "^3.0.4" + compress-commons "^4.1.2" readable-stream "^3.6.0" diff --git a/cla-backend/.gitignore b/cla-backend/.gitignore index d03236b63..da4d52a37 100644 --- a/cla-backend/.gitignore +++ b/cla-backend/.gitignore @@ -1,5 +1,6 @@ # Copyright The Linux Foundation and each contributor to CommunityBridge. # SPDX-License-Identifier: MIT +bin/ wsgi.py serverless_wsgi.py wsgi_handler.py @@ -13,16 +14,5 @@ _env.json .mypy_cache .venv .vscode/ - -# Golang binaries that may have been copied over for local deployment -backend-aws-lambda -dynamo-events-lambda -functional-tests -metrics-aws-lambda -user-subscribe-lambda -zipbuilder-lambda -zipbuilder-scheduler-lambda -zipbuilder-lambda-mac -zipbuilder-scheduler-lambda-mac - +.coverage* diff --git a/cla-backend/.python-version b/cla-backend/.python-version new file mode 100644 index 000000000..f7e5aa84c --- /dev/null +++ b/cla-backend/.python-version @@ -0,0 +1 @@ +3.7.12 diff --git a/cla-backend/cla/config.py b/cla-backend/cla/config.py index b143d5cd9..eb33a0683 100644 --- a/cla-backend/cla/config.py +++ b/cla-backend/cla/config.py @@ -52,6 +52,9 @@ def get_ssm_key(region, key): # The linux foundation is the parent for many SF projects THE_LINUX_FOUNDATION = 'The Linux Foundation' +# LF Projects LLC is the parent for many SF projects +LF_PROJECTS_LLC = 'LF Projects, LLC' + # Base URL used for callbacks and OAuth2 redirects. API_BASE_URL = os.environ.get('CLA_API_BASE', '') @@ -61,6 +64,7 @@ def get_ssm_key(region, key): # Corporate Console base URL CORPORATE_BASE_URL = os.environ.get('CLA_CORPORATE_BASE', '') +CORPORATE_V2_BASE_URL = os.environ.get('CLA_CORPORATE_V2_BASE', '') # Landing Page CLA_LANDING_PAGE = os.environ.get('CLA_LANDING_PAGE', '') @@ -126,6 +130,7 @@ def get_ssm_key(region, key): # PDF Generation. PDF_SERVICE = 'DocRaptor' + AUTH0_PLATFORM_URL = os.getenv("AUTH0_PLATFORM_URL", "") AUTH0_PLATFORM_CLIENT_ID = os.getenv("AUTH0_PLATFORM_CLIENT_ID", "") AUTH0_PLATFORM_CLIENT_SECRET = os.getenv("AUTH0_PLATFORM_CLIENT_SECRET", "") @@ -137,6 +142,15 @@ def get_ssm_key(region, key): # property on class construction GITHUB_PRIVATE_KEY = "" +# DocuSign Private Key +DOCUSIGN_PRIVATE_KEY = "" + +#Docusign Integration Key +DOCUSIGN_INTEGRATOR_KEY = "" + +#Oocusign user id +DOCUSIGN_USER_ID = "" + # reference to this module, cla.config this = sys.modules[__name__] @@ -164,7 +178,10 @@ def _load_single_key(key): f'cla-auth0-platform-url-{stage}', f'cla-auth0-platform-client-id-{stage}', f'cla-auth0-platform-client-secret-{stage}', - f'cla-auth0-platform-audience-{stage}' + f'cla-auth0-platform-audience-{stage}', + f'cla-docusign-private-key-{stage}', + f'cla-docusign-integrator-key-{stage}', + f'cla-docusign-user-id-{stage}' ] config_keys = [ "GITHUB_PRIVATE_KEY", @@ -172,11 +189,14 @@ def _load_single_key(key): "AUTH0_PLATFORM_URL", "AUTH0_PLATFORM_CLIENT_ID", "AUTH0_PLATFORM_CLIENT_SECRET", - "AUTH0_PLATFORM_AUDIENCE" + "AUTH0_PLATFORM_AUDIENCE", + "DOCUSIGN_PRIVATE_KEY", + "DOCUSIGN_INTEGRATOR_KEY", + "DOCUSIGN_USER_ID" ] - # thread pool of 5 to load fetch the keys - pool = ThreadPool(5) + # thread pool of 7 to load fetch the keys + pool = ThreadPool(7) results = pool.map(_load_single_key, keys) pool.close() pool.join() diff --git a/cla-backend/cla/controllers/company.py b/cla-backend/cla/controllers/company.py index 1f9424d78..6535eddf2 100644 --- a/cla-backend/cla/controllers/company.py +++ b/cla-backend/cla/controllers/company.py @@ -83,10 +83,10 @@ def get_company(company_id: str): def create_company(auth_user: AuthUser, company_name: str = None, signing_entity_name: str = None, - company_manager_id=None, - company_manager_user_name=None, - company_manager_user_email=None, - user_id=None, + company_manager_id: str = None, + company_manager_user_name: str = None, + company_manager_user_email: str = None, + user_id: str = None, response=None): """ Creates an company and returns the newly created company in dict format. @@ -118,7 +118,7 @@ def create_company(auth_user: AuthUser, "company_id": company.get("company_id")} } - cla.log.debug(f'{fn} - creating company with name: {company_name}') + cla.log.debug(f'{fn} - creating company with name: {company_name} with signing entity name: {signing_entity_name}') company = Company() company.set_company_id(str(uuid.uuid4())) company.set_company_name(company_name) diff --git a/cla-backend/cla/controllers/github.py b/cla-backend/cla/controllers/github.py index a2a3bff32..fa498066b 100644 --- a/cla-backend/cla/controllers/github.py +++ b/cla-backend/cla/controllers/github.py @@ -284,6 +284,10 @@ def activity(action: str, event_type: str, body: dict): elif event_type == "issue_comment": cla.log.debug(f'{fn} - received issue_comment action: {action}...') handle_pull_request_comment_event(action, body) + + # Github Merge Group Event + elif event_type == 'merge_group': + handle_merge_group_event(action, body) else: cla.log.debug(f'{fn} - ignoring github activity event, action: {action}...') @@ -350,7 +354,7 @@ def handle_pull_request_event(action: str, body: dict): cla.log.debug(f'{func_name} - processing github pull_request activity callback...') # New PR opened - if action == 'opened' or action == 'reopened' or action == 'synchronize': + if action == 'opened' or action == 'reopened' or action == 'synchronize' or action == 'enqueued': cla.log.debug(f'{func_name} - processing github pull_request activity for action: {action}') # Copied from repository_service.py service = cla.utils.get_repository_service('github') @@ -359,6 +363,20 @@ def handle_pull_request_event(action: str, body: dict): else: cla.log.debug(f'{func_name} - ignoring github pull_request activity for action: {action}') +def handle_merge_group_event(action: str, body: dict): + func_name = 'github.activity.handle_merge_group_event' + cla.log.debug(f'{func_name} - processing github merge_group activity callback...') + + # Checks Requested action + if action == 'checks_requested': + cla.log.debug(f'{func_name} - processing github merge_group activity for action: {action}') + # Copied from repository_service.py + service = cla.utils.get_repository_service('github') + result = service.received_activity(body) + return result + else: + cla.log.debug(f'{func_name} - ignoring github merge_group activity for action: {action}') + def handle_pull_request_comment_event(action: str, body: dict): func_name = 'github.activity.handle_pull_request_comment_event' @@ -372,7 +390,7 @@ def handle_pull_request_comment_event(action: str, body: dict): result = service.process_easycla_command_comment(body) return result except ValueError as ex: - cla.log.warning(f"process_easycla_command_comment failed with : {str(ex)}") + cla.log.debug(f'{func_name} - ignoring github pull_request comment: {str(ex)}') return None else: cla.log.debug(f'{func_name} - ignoring github pull_request comment activity for action: {action}') @@ -480,8 +498,7 @@ def handle_installation_repositories_added_event(action: str, body: dict): 'to the CLA configuration. GitHub organization was set to auto-enable.') Event.create_event( event_type=EventType.RepositoryAdded, - event_project_id=cla_group_id, - event_project_name=project_model.get_project_name(), + event_cla_group_id=cla_group_id, event_company_id=None, event_data=msg, event_summary=msg, @@ -544,8 +561,7 @@ def handle_installation_repositories_removed_event(action: str, body: dict): # Log the event Event.create_event( event_type=EventType.RepositoryDisable, - event_project_id=repo.get_repository_project_id(), - event_project_name=project_model.get_project_name(), + event_cla_group_id=repo.get_repository_project_id(), event_company_id=None, event_data=msg, event_summary=msg, @@ -583,6 +599,7 @@ def notify_project_managers(repositories): f' to managers: {recipients}' f' for project {project} with ' f' repositories: {repositories}') + def unable_to_do_cla_check_email_content(project, managers, repositories): diff --git a/cla-backend/cla/controllers/project.py b/cla-backend/cla/controllers/project.py index 45c512728..269768ddc 100644 --- a/cla-backend/cla/controllers/project.py +++ b/cla-backend/cla/controllers/project.py @@ -224,7 +224,8 @@ def create_project(project_external_id, project_name, project_icla_enabled, proj event_data = 'Project-{} created'.format(project_name) Event.create_event( event_type=EventType.CreateProject, - event_project_id=project.get_project_id(), + event_cla_group_id=project.get_project_id(), + event_project_id=project_external_id, event_data=event_data, event_summary=event_data, contains_pii=False, @@ -277,7 +278,7 @@ def update_project(project_id, project_name=None, project_icla_enabled=None, event_data = f'Project- {project_id} Updates: ' + updated_string Event.create_event( event_type=EventType.UpdateProject, - event_project_id=project.get_project_id(), + event_cla_group_id=project.get_project_id(), event_data=event_data, event_summary=event_data, contains_pii=False, @@ -304,7 +305,7 @@ def delete_project(project_id, username=None): event_data = 'Project-{} deleted'.format(project.get_project_name()) Event.create_event( event_type=EventType.DeleteProject, - event_project_id=project_id, + event_cla_group_id=project_id, event_data=event_data, event_summary=event_data, contains_pii=False, @@ -476,7 +477,7 @@ def post_project_document(project_id, event_data = 'Created new document for Project-{} '.format(project.get_project_name()) Event.create_event( event_type=EventType.CreateProjectDocument, - event_project_id=project.get_project_id(), + event_cla_group_id=project.get_project_id(), event_data=event_data, event_summary=event_data, contains_pii=False, @@ -554,7 +555,7 @@ def post_project_document_template(project_id, project.get_project_name(), template_name) Event.create_event( event_type=EventType.CreateProjectDocumentTemplate, - event_project_id=project.get_project_id(), + event_cla_group_id=project.get_project_id(), event_data=event_data, event_summary=event_data, contains_pii=False, @@ -598,7 +599,7 @@ def delete_project_document(project_id, document_type, major_version, minor_vers Event.create_event( event_data=event_data, event_summary=event_data, - event_project_id=project_id, + event_cla_group_id=project_id, event_type=EventType.DeleteProjectDocument, contains_pii=False, ) @@ -876,7 +877,7 @@ def add_project_manager(username, project_id, lfid): event_type=EventType.AddProjectManager, event_data=event_data, event_summary=event_data, - event_project_id=project_id, + event_cla_group_id=project_id, contains_pii=True, ) @@ -928,7 +929,7 @@ def remove_project_manager(username, project_id, lfid): event_type=EventType.RemoveProjectManager, event_data=event_data, event_summary=event_data, - event_project_id=project_id, + event_cla_group_id=project_id, contains_pii=True, ) diff --git a/cla-backend/cla/controllers/signature.py b/cla-backend/cla/controllers/signature.py index 74d2446e8..bd6e61e9f 100644 --- a/cla-backend/cla/controllers/signature.py +++ b/cla-backend/cla/controllers/signature.py @@ -136,7 +136,7 @@ def create_signature(signature_project_id, # pylint: disable=too-many-arguments event_data=event_data, event_summary=event_data, event_type=EventType.CreateSignature, - event_project_id=signature_project_id, + event_cla_group_id=str(signature_project_id), contains_pii=False, ) @@ -303,6 +303,7 @@ def update_signature(signature_id, # pylint: disable=too-many-arguments,too-man event_data=event_data, event_summary=event_data, event_type=EventType.UpdateSignature, + event_cla_group_id=signature.get_signature_project_id(), contains_pii=True, ) @@ -390,6 +391,7 @@ def notify_whitelist_change(auth_user, old_signature: Signature, new_signature: event_type=EventType.NotifyWLChange, event_company_name=company_name, event_project_name=project_name, + event_cla_group_id=new_signature.get_signature_project_id(), contains_pii=True, ) @@ -688,8 +690,10 @@ def delete_signature(signature_id): :type signature_id: UUID """ signature = Signature() + cla_group_id = '' try: # Try to load the signature to delete. signature.load(str(signature_id)) + cla_group_id = signature.get_signature_project_id() except DoesNotExist as err: # Should we bother sending back an error? return {'errors': {'signature_id': str(err)}} @@ -698,6 +702,7 @@ def delete_signature(signature_id): Event.create_event( event_data=event_data, event_summary=event_data, + event_cla_group_id=cla_group_id, event_type=EventType.DeleteSignature, contains_pii=False, ) @@ -976,6 +981,7 @@ def add_cla_manager(auth_user: AuthUser, signature_id: str, lfid: str): event_data = f'{lfid} added as cla manager to Signature ACL for {signature.get_signature_id()}' Event.create_event( event_data=event_data, + event_cla_group_id=signature.get_signature_project_id(), event_summary=event_data, event_type=EventType.AddCLAManager, contains_pii=True, @@ -1041,6 +1047,7 @@ def remove_cla_manager(username, signature_id, lfid): event_data=event_data, event_summary=event_data, event_type=EventType.RemoveCLAManager, + event_cla_group_id=project.get_project_id(), contains_pii=True, ) diff --git a/cla-backend/cla/controllers/signing.py b/cla-backend/cla/controllers/signing.py index f283bf6a4..dfe0e2951 100644 --- a/cla-backend/cla/controllers/signing.py +++ b/cla-backend/cla/controllers/signing.py @@ -11,7 +11,7 @@ import cla from cla.models import DoesNotExist -from cla.models.dynamo_models import Signature +from cla.models.dynamo_models import Signature, User from cla.user_service import UserService from cla.utils import get_signing_service, get_signature_instance, get_email_service, \ get_repository_service, get_project_instance, get_company_instance @@ -36,23 +36,33 @@ def request_individual_signature(project_id, user_id, return_url_type, return_ur signing_service = get_signing_service() if return_url_type is not None and return_url_type.lower() == "gerrit": return signing_service.request_individual_signature_gerrit(str(project_id), str(user_id), return_url) - elif return_url_type is not None and return_url_type.lower() == "github": - # fetching the primary for the account - github = get_repository_service("github") - primary_user_email = github.get_primary_user_email(request) - return signing_service.request_individual_signature(str(project_id), str(user_id), return_url, + elif return_url_type is not None and (return_url_type.lower() == "github" or return_url_type.lower() == "gitlab"): + if return_url_type.lower() == "github": + # fetching the primary for the account + github = get_repository_service("github") + primary_user_email = github.get_primary_user_email(request) + elif return_url_type.lower() == "gitlab": + try: + cla.log.debug(f"Fetching user details for: {user_id}") + user = User() + user.load(user_id) + except DoesNotExist as err: + cla.log.warning('Individual Signature - user ID was NOT found for: {}'.format(user_id)) + return {'errors': {'user_id': str(err)}} + primary_user_email = user.get_user_email() + return signing_service.request_individual_signature(str(project_id), str(user_id), return_url, return_url_type, preferred_email=primary_user_email) def request_corporate_signature(auth_user, - project_id, - company_id, + project_id: str, + company_id: str, signing_entity_name: str = None, - send_as_email=False, - authority_name=None, - authority_email=None, - return_url_type=None, - return_url=None): + send_as_email: bool = False, + authority_name: str = None, + authority_email: str = None, + return_url_type: str = None, + return_url: str = None): """ Creates CCLA signature object that represents a company signing a CCLA. @@ -80,8 +90,8 @@ def request_corporate_signature(auth_user, """ return get_signing_service().request_corporate_signature( auth_user=auth_user, - project_id=str(project_id), - company_id=str(company_id), + project_id=project_id, + company_id=company_id, signing_entity_name=signing_entity_name, send_as_email=send_as_email, signatory_name=authority_name, @@ -104,13 +114,24 @@ def request_employee_signature(project_id, company_id, user_id, return_url_type, :type return_url_type: string :param return_url: The URL to return the user to after signing is complete. """ + fn = 'cla.controllers.signing.request_employee_signature' signing_service = get_signing_service() if return_url_type is not None and return_url_type.lower() == "gerrit": + cla.log.error(f'{fn} - return type is gerrit - invoking: request_employee_signature_gerrit') return signing_service.request_employee_signature_gerrit(str(project_id), str(company_id), str(user_id), return_url) - elif return_url_type is not None and return_url_type.lower() == "github": - return signing_service.request_employee_signature(str(project_id), str(company_id), str(user_id), return_url) + elif return_url_type is not None and (return_url_type.lower() == "github" or return_url_type.lower() == "gitlab"): + cla.log.error(f'{fn} - return type is github - invoking: request_employee_signature') + return signing_service.request_employee_signature(str(project_id), str(company_id), str(user_id), return_url, return_url_type=return_url_type) + + else: + msg = (f'{fn} - unsupported return type {return_url_type} for ' + f'cla group: {project_id}, ' + f'company: {company_id}, ' + f'user: {user_id}') + cla.log.error(msg) + raise falcon.HTTPBadRequest(title=msg) def check_and_prepare_employee_signature(project_id, company_id, user_id): @@ -169,6 +190,17 @@ def post_individual_signed(content, installation_id, github_repository_id, chang """ get_signing_service().signed_individual_callback(content, installation_id, github_repository_id, change_request_id) +def post_individual_signed_gitlab(content, user_id, organization_id, gitlab_repository_id, merge_request_id): + """ + Handle the posted callback from the signing service after ICLA signature. + + :param content: The POST body from the signing service callback. + :type content: string + :param user_id: The ID of the user that signed. + :type user_id: string + """ + get_signing_service().signed_individual_callback_gitlab(content,user_id, organization_id, gitlab_repository_id, merge_request_id) + def post_individual_signed_gerrit(content, user_id): """ diff --git a/cla-backend/cla/controllers/user.py b/cla-backend/cla/controllers/user.py index 0a7d6278f..9d4e2fbbd 100644 --- a/cla-backend/cla/controllers/user.py +++ b/cla-backend/cla/controllers/user.py @@ -193,7 +193,7 @@ def request_company_whitelist(user_id: str, company_id: str, user_name: str, use f'as {user_name} <{user_email}>') Event.create_event( event_user_id=user_id, - event_project_id=project_id, + event_cla_group_id=project_id, event_company_id=company_id, event_type=EventType.RequestCompanyWL, event_data=event_data, @@ -288,6 +288,7 @@ def invite_cla_manager(contributor_id, contributor_name, contributor_email, cla_ event_data=log_msg, event_summary=log_msg, event_type=EventType.InviteAdmin, + event_cla_group_id=project.get_project_id(), contains_pii=True, ) @@ -331,6 +332,7 @@ def request_company_ccla(user_id, user_email, company_id, project_id): event_type=EventType.RequestCCLA, event_user_id=user_id, event_company_id=company_id, + event_cla_group_id=project.get_project_id(), contains_pii=False, ) @@ -433,8 +435,11 @@ def get_user_project_last_signature(user_id, project_id): latest_doc = cla.utils.get_project_latest_individual_document(str(project_id)) last_signature['latest_document_major_version'] = str(latest_doc.get_document_major_version()) last_signature['latest_document_minor_version'] = str(latest_doc.get_document_minor_version()) - last_signature['requires_resigning'] = last_signature['latest_document_major_version'] != last_signature[ - 'signature_document_major_version'] and last_signature['signature_signed'] + last_signature['requires_resigning'] = False + if last_signature['signature_signed'] == False: + last_signature['requires_resigning'] = True + elif last_signature['latest_document_major_version'] != last_signature['signature_document_major_version']: + last_signature['requires_resigning'] = True return last_signature diff --git a/cla-backend/cla/docusign_auth.py b/cla-backend/cla/docusign_auth.py new file mode 100644 index 000000000..ebc8ddbec --- /dev/null +++ b/cla-backend/cla/docusign_auth.py @@ -0,0 +1,63 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +""" +docusign_auth.py contains all necessary objects and functions to perform authentication and authorization. +""" + + +import requests +import os +import jwt +from time import time +import cla +import math + + +INTEGRATION_KEY = cla.config.DOCUSIGN_INTEGRATOR_KEY +INTEGRATION_SECRET = cla.config.DOCUSIGN_PRIVATE_KEY +USER_ID = cla.config.DOCUSIGN_USER_ID +OAUTH_BASE_URL = os.environ.get('DOCUSIGN_AUTH_SERVER') + + +def request_access_token() -> str: + """ + Requests an access token from the DocuSign OAuth2 service. + """ + try: + cla.log.debug('Requesting access token from DocuSign OAuth2 service...') + url = f'https://{OAUTH_BASE_URL}/oauth/token' + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + claims = { + "iss": INTEGRATION_KEY, + "sub": USER_ID, + "aud": OAUTH_BASE_URL, + "iat": time(), + "exp": time() + 3600, + "scope": "signature impersonation" + } + cla.log.debug(f'Claims: {claims}') + encoded_jwt = jwt.encode(claims, INTEGRATION_SECRET.encode(), algorithm='RS256') + + payload = { + 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'assertion': encoded_jwt + } + + response = requests.post(url, headers=headers, data=payload) + data = response.json() + if 'token_type' in data and 'access_token' in data: + cla.log.debug('Successfully requested access token from DocuSign OAuth2 service.') + return data['access_token'] + else: + cla.log.error('Unable to request access token from DocuSign OAuth2 service: ' + str(data)) + raise Exception('Unable to request access token from DocuSign OAuth2 service: ' + str(data)) + + except Exception as err: + cla.log.error('Unable to request access token from DocuSign OAuth2 service: ' + str(err)) + raise err + + + diff --git a/cla-backend/cla/middleware.py b/cla-backend/cla/middleware.py new file mode 100644 index 000000000..7c5b072e9 --- /dev/null +++ b/cla-backend/cla/middleware.py @@ -0,0 +1,29 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +from hug.middleware import LogMiddleware +from datetime import datetime +from timeit import default_timer + +class CLALogMiddleware(LogMiddleware): + """CLA log middleware""" + + def __init__(self, logger=None): + super().__init__(logger=logger) + self.elapsed_time = 0 + self.start_time = None + self.end_time = None + + def process_request(self, request, response): + """Logs CLA request """ + self.logger.info(f'BEGIN {request.method} {request.path}') + self.start_time = datetime.utcnow() + super().process_request(request, response) + + def process_response(self, request, response, resource, req_succeeded): + """Logs data returned by CLA API """ + if self.start_time: + self.elapsed_time = datetime.utcnow() - self.start_time + super().process_response(request, response, resource, req_succeeded) + self.logger.info(f'END {request.method} {request.path} - elapsed_time : {self.elapsed_time.seconds} secs') + diff --git a/cla-backend/cla/models/docusign_models.py b/cla-backend/cla/models/docusign_models.py index e0b67edd6..de52401c4 100644 --- a/cla-backend/cla/models/docusign_models.py +++ b/cla-backend/cla/models/docusign_models.py @@ -10,27 +10,32 @@ """ import io +import json +import boto3 import os import urllib.request import uuid import xml.etree.ElementTree as ET -from typing import Dict, Any, Optional +from typing import Any, Dict, List, Optional from urllib.parse import urlparse - -import pydocusign # type: ignore -from pydocusign.exceptions import DocuSignException # type: ignore +from datetime import datetime import cla +import pydocusign # type: ignore +import requests +from attr import dataclass from cla.controllers.lf_group import LFGroup -from cla.models import signing_service_interface, DoesNotExist -from cla.models.dynamo_models import Signature, User, \ - Project, Company, Gerrit, \ - Document, Event +from cla.models import DoesNotExist, signing_service_interface +from cla.models.dynamo_models import (Company, Document, Event, Gerrit, + Project, Signature, User) from cla.models.event_types import EventType from cla.models.s3_storage import S3Storage from cla.user_service import UserService -from cla.utils import get_email_help_content, append_email_help_sign_off_content +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 +stage = os.environ.get('STAGE', '') api_base_url = os.environ.get('CLA_API_BASE', '') root_url = os.environ.get('DOCUSIGN_ROOT_URL', '') username = os.environ.get('DOCUSIGN_USERNAME', '') @@ -43,6 +48,8 @@ lf_group_refresh_token = os.environ.get('LF_GROUP_REFRESH_TOKEN', '') lf_group = LFGroup(lf_group_client_url, lf_group_client_id, lf_group_client_secret, lf_group_refresh_token) +signature_table = 'cla-{}-signatures'.format(stage) + class ProjectDoesNotExist(Exception): pass @@ -97,8 +104,10 @@ class DocuSign(signing_service_interface.SigningService): def __init__(self): self.client = None self.s3storage = None + self.dynamo_client = None def initialize(self, config): + self.dynamo_client = boto3.client('dynamodb') self.client = pydocusign.DocuSignClient(root_url=root_url, username=username, password=password, @@ -124,7 +133,7 @@ def initialize(self, config): self.s3storage = S3Storage() self.s3storage.initialize(None) - def request_individual_signature(self, project_id, user_id, return_url=None, callback_url=None, + def request_individual_signature(self, project_id, user_id, return_url=None, return_url_type="github", callback_url=None, preferred_email=None): request_info = 'project: {project_id}, user: {user_id} with return_url: {return_url}'.format( project_id=project_id, user_id=user_id, return_url=return_url) @@ -177,7 +186,11 @@ def request_individual_signature(self, project_id, user_id, return_url=None, cal cla.log.debug('Individual Signature - get active signature metadata: {}'.format(signature_metadata)) cla.log.debug('Individual Signature - get individual signature callback url') - callback_url = cla.utils.get_individual_signature_callback_url(user_id, signature_metadata) + if return_url_type.lower() == "github": + callback_url = cla.utils.get_individual_signature_callback_url(user_id, signature_metadata) + elif return_url_type.lower() == "gitlab": + callback_url = cla.utils.get_individual_signature_callback_url_gitlab(user_id, signature_metadata) + cla.log.debug('Individual Signature - get individual signature callback url: {}'.format(callback_url)) if latest_signature is not None and \ @@ -238,15 +251,19 @@ def request_individual_signature(self, project_id, user_id, return_url=None, cal signature_reference_type='user', signature_reference_name=user.get_user_name(), signature_type='cla', - signature_return_url_type='Github', + signature_return_url_type=return_url_type, signature_signed=False, signature_approved=True, signature_return_url=return_url, signature_callback_url=callback_url) # Set signature ACL - cla.log.debug('Individual Signature - setting ACL using user GH id: {}'.format(user.get_user_github_id())) - signature.set_signature_acl('github:{}'.format(user.get_user_github_id())) + if return_url_type.lower() == "github": + acl = user.get_user_github_id() + elif return_url_type.lower() == "gitlab": + acl = user.get_user_gitlab_id() + cla.log.debug('Individual Signature - setting ACL using user {} id: {}'.format(return_url_type, acl)) + signature.set_signature_acl('{}:{}'.format(return_url_type.lower(),acl)) # Populate sign url self.populate_sign_url(signature, callback_url, default_values=default_cla_values, @@ -370,7 +387,10 @@ def check_and_prepare_employee_signature(project_id, company_id, user_id) -> dic # Returns an error if any of the above is false. fn = 'docusign_models.check_and_prepare_employee_signature' - request_info = f'project: {project_id}, company: {company_id}, user: {user_id}' + # Keep a variable with the actual company_id - may swap the original selected company id to use another + # company id if another signing entity name (another related company) is already signed + actual_company_id = company_id + request_info = f'project: {project_id}, company: {actual_company_id}, user: {user_id}' cla.log.info(f'{fn} - check and prepare employee signature for {request_info}') # Ensure the project exists @@ -386,12 +406,12 @@ def check_and_prepare_employee_signature(project_id, company_id, user_id) -> dic # Ensure the company exists company = Company() try: - cla.log.debug(f'{fn} - loading company by id: {company_id}...') - company.load(str(company_id)) + cla.log.debug(f'{fn} - loading company by id: {actual_company_id}...') + company.load(str(actual_company_id)) cla.log.debug(f'{fn} - company {company.get_company_name()} exists for: {request_info}') except DoesNotExist: cla.log.warning(f'{fn} - company does NOT exist for: {request_info}') - return {'errors': {'company_id': f'Company ({company_id}) does not exist.'}} + return {'errors': {'company_id': f'Company ({actual_company_id}) does not exist.'}} # Ensure the user exists user = User() @@ -416,9 +436,135 @@ def check_and_prepare_employee_signature(project_id, company_id, user_id) -> dic project_id=project_id ) if len(ccla_signatures) < 1: - cla.log.warning(f'{fn} - project {project.get_project_name()} and ' - f'company {company.get_company_name()} does not have CCLA for: {request_info}') - return {'errors': {'missing_ccla': 'Company does not have CCLA with this project'}} + # Save our message + msg = (f'{fn} - project {project.get_project_name()} and ' + f'company {company.get_company_name()} does not have CCLA for: {request_info}') + cla.log.debug(msg) + + return {'errors': {'missing_ccla': 'Company does not have CCLA with this project.', + 'company_id': actual_company_id, + 'company_name': company.get_company_name(), + 'signing_entity_name': company.get_signing_entity_name(), + 'company_external_id': company.get_company_external_id(), + } + } + # # Ok - long story here, we could have the tricky situation where now that we've added a concept of Signing + # # Entity Names we have, basically, a set of 'child' companies all under a common external_id (SFID). This + # # would have been so much simpler if SF supported Parent/Child company relationships to model things like + # # Subsidiary and Patten holding companies. + # # + # # Scenario: + # # + # # Deal Company (SFID: 123, CompanyID: AAA) + # # Deal Company Subsidiary 1 - (SFID: 123, CompanyID: BBB) + # # Deal Company Subsidiary 2 - (SFID: 123, CompanyID: CCC) - SIGNED! + # # Deal Company Subsidiary 3 - (SFID: 123, CompanyID: DDD) + # # Deal Company Subsidiary 4 - (SFID: 123, CompanyID: EEE) + # # + # # Now - the check-prepare-employee signature request could have come from any of the above companies with + # # different a company_id - the contributor may have selected the correct option (CCC), the one that was + # # signed and executed by a Signatory...or maybe none have been signed...or perhaps another one was signed + # # such as companyID BBB. + # # + # # Originally, we designed the system to keep track of all these sub-companies separately - different CLA + # # managers, different approval lists, etc. + # # + # # Later, the stakeholders wanted to group these all together as one but keep track of the signing entity + # # name for each project | company. They wanted to allow the users to select one for each (project | + # # organization) pair. + # # + # # So, we could have CLA signatories/managers wanting: + # # + # # - Project OpenCue + Deal Company Subsidiary 2 + # # - Project OpenVDB + Deal Company Subsidiary 4 + # # - Project OpenTelemetry + Deal Company + # # + # # As a result, we need to query the entire company family under the same external_id for a signed CCLA. + # # Currently, we only allow 1 of these to be signed for each Project | Company pair. Later, we may change + # # this behavior (it's been debated). + # # + # # Let's see if they signed the CCLA for another of the Company/Signed Entity Names for this + # # project - if so, let's return that one, if not, return the error + # + # # First, grab the current company's external ID/SFID + # company_external_id = company.get_company_external_id() + # # if missing, not much we can do... + # if company_external_id is None: + # cla.log.warning(f'{fn} - project {project.get_project_name()} and ' + # f'company {company.get_company_name()} - company missing external id - ' + # f'{request_info}') + # cla.log.warning(msg) + # return {'errors': {'missing_ccla': 'Company does not have CCLA with this project.', + # 'company_id': actual_company_id, + # 'company_name': company.get_company_name(), + # 'signing_entity_name': company.get_signing_entity_name(), + # 'company_external_id': company.get_company_external_id(), + # } + # } + # + # # Lookup the other companies by external id...will have 1 or more (current record plus possibly others)... + # company_list = company.get_company_by_external_id(company_external_id) + # # This shouldn't happen, let's trap for it anyway + # if not company_list: + # cla.log.warning(f'{fn} - project {project.get_project_name()} and ' + # f'company {company.get_company_name()} - unable to lookup companies by external id: ' + # f'{company_external_id} - {request_info}') + # cla.log.warning(msg) + # return {'errors': {'missing_ccla': 'Company does not have CCLA with this project.', + # 'company_id': actual_company_id, + # 'company_name': company.get_company_name(), + # 'signing_entity_name': company.get_signing_entity_name(), + # 'company_external_id': company.get_company_external_id(), + # } + # } + # + # # As we loop, let's use a flag to keep track if we find a CCLA + # found_ccla = False + # for other_company in company_list: + # cla.log.debug(f'{fn} - loading CCLA signatures by cla group: {project.get_project_name()} ' + # f'and company id: {other_company.get_company_id()}...') + # ccla_signatures = Signature().get_ccla_signatures_by_company_project( + # company_id=other_company.get_company_id(), + # project_id=project_id + # ) + # + # # Do we have a signed CCLA for this project|company ? If so, we found it - use it! Should NOT have + # # more than one of the companies with Signed CCLAs + # if len(ccla_signatures) > 0: + # found_ccla = True + # # Need to load the correct company record + # try: + # # Reset the actual company id value since we found a CCLA under a related signing entity name + # # company + # actual_company_id = ccla_signatures[0].get_signature_reference_id() + # # Reset the request_info string with the updated company_id, will use it for debug/warning below + # request_info = f'project: {project_id}, company: {actual_company_id}, user: {user_id}' + # cla.log.debug(f'{fn} - loading correct signed CCLA company by id: ' + # f'{ccla_signatures[0].get_signature_reference_id()} ' + # f'with signed entity name: {ccla_signatures[0].get_signing_entity_name()} ...') + # company.load(ccla_signatures[0].get_signature_reference_id()) + # cla.log.debug(f'{fn} - loaded company {company.get_company_name()} ' + # f'with signing entity name: {company.get_signing_entity_name()} ' + # f'for {request_info}.') + # except DoesNotExist: + # cla.log.warning(f'{fn} - company does NOT exist ' + # f'using company_id: {ccla_signatures[0].get_signature_reference_id()} ' + # f'for: {request_info}') + # return {'errors': {'company_id': f'Company ({ccla_signatures[0].get_signature_reference_id()}) ' + # 'does not exist.'}} + # break + # + # # if we didn't fine a signed CCLA under any of the other companies... + # if not found_ccla: + # # Give up + # cla.log.warning(msg) + # return {'errors': {'missing_ccla': 'Company does not have CCLA with this project.', + # 'company_id': actual_company_id, + # 'company_name': company.get_company_name(), + # 'signing_entity_name': company.get_signing_entity_name(), + # 'company_external_id': company.get_company_external_id(), + # } + # } # Add a note in the log if we have more than 1 signed and approved CCLA signature if len(ccla_signatures) > 1: @@ -436,14 +582,22 @@ def check_and_prepare_employee_signature(project_id, company_id, user_id) -> dic if not user.is_approved(ccla_signature): # TODO: DAD - update this warning message cla.log.warning(f'{fn} - user is not authorized for this CCLA: {request_info}') - return {'errors': {'ccla_approval_list': 'user not authorized for this ccla'}} + return {'errors': {'ccla_approval_list': 'user not authorized for this ccla', + 'company_id': actual_company_id, + 'company_name': company.get_company_name(), + 'signing_entity_name': company.get_signing_entity_name(), + 'company_external_id': company.get_company_external_id(), + } + } cla.log.info(f'{fn} - user is approved for this CCLA: {request_info}') - # Assume this company is the user's employer. + # Assume this company is the user's employer. Associated the company with the user in the EasyCLA user record + # For v2, we make the association with the platform via the platform project service via a separate API + # call from the UI # TODO: DAD - we should check to see if they already have a company id assigned - if user.get_user_company_id() != company_id: - user.set_user_company_id(str(company_id)) + if user.get_user_company_id() != actual_company_id: + user.set_user_company_id(str(actual_company_id)) event_data = (f'The user {user.get_user_name()} with GitHub username ' f'{user.get_github_username()} (' f'{user.get_user_github_id()}) and user ID ' @@ -456,11 +610,12 @@ def check_and_prepare_employee_signature(project_id, company_id, user_id) -> dic f'project {project.get_project_name()}.') Event.create_event( event_type=EventType.UserAssociatedWithCompany, - event_company_id=company_id, + event_company_id=actual_company_id, event_company_name=company.get_company_name(), - event_project_id=project_id, + event_cla_group_id=project_id, event_project_name=project.get_project_name(), event_user_id=user.get_user_id(), + event_user_name=user.get_user_name() if user else None, event_data=event_data, event_summary=event_summary, contains_pii=True, @@ -489,15 +644,16 @@ def check_and_prepare_employee_signature(project_id, company_id, user_id) -> dic return {'success': {'the employee is ready to sign the CCLA'}} - def request_employee_signature(self, project_id, company_id, user_id, return_url=None): + def request_employee_signature(self, project_id, company_id, user_id, return_url=None, return_url_type="github"): - request_info = f'project: {project_id}, company: {company_id}, user: {user_id} with return_url: {return_url}' - cla.log.info(f'Processing request_employee_signature request with {request_info}') + fn = 'docusign_models.check_and_prepare_employee_signature' + request_info = f'cla group: {project_id}, company: {company_id}, user: {user_id} with return_url: {return_url}' + cla.log.info(f'{fn} - processing request_employee_signature request with {request_info}') check_and_prepare_signature = self.check_and_prepare_employee_signature(project_id, company_id, user_id) # Check if there are any errors while preparing the signature. if 'errors' in check_and_prepare_signature: - cla.log.warning(f'Error in check_and_prepare_signature with: {request_info} - ' + cla.log.warning(f'{fn} - error in check_and_prepare_signature with: {request_info} - ' f'signatures: {check_and_prepare_signature}') return check_and_prepare_signature @@ -505,28 +661,30 @@ def request_employee_signature(self, project_id, company_id, user_id, return_url company_id=company_id, project_id=project_id, user_id=user_id) # Return existing signature if employee has signed it if employee_signature is not None: - cla.log.info(f'Employee has signed for company: {company_id}, ' - f'request_info: {request_info} - signature: {employee_signature}') + cla.log.info(f'{fn} - employee has previously acknowledged their company affiliation ' + f'for request_info: {request_info} - signature: {employee_signature}') return employee_signature.to_dict() - cla.log.info(f'Employee has NOT signed it for: {request_info}') + cla.log.info(f'{fn} - employee has NOT previously acknowledged their company affiliation for : {request_info}') # Requires us to know where the user came from. signature_metadata = cla.utils.get_active_signature_metadata(user_id) if return_url is None: - cla.log.info(f'No return URL for: {request_info}') + cla.log.debug(f'{fn} - no return URL for: {request_info}') return_url = cla.utils.get_active_signature_return_url(user_id, signature_metadata) - cla.log.info(f'Set return URL for: {request_info} to: {return_url}') + cla.log.debug(f'{fn} - set return URL for: {request_info} to: {return_url}') # project has already been checked from check_and_prepare_employee_signature. Load project with project ID. project = Project() + cla.log.info(f'{fn} - loading cla group details for: {request_info}') project.load(project_id) - cla.log.info(f'Loaded project details for: {request_info}') + cla.log.info(f'{fn} - loaded cla group details for: {request_info}') # company has already been checked from check_and_prepare_employee_signature. Load company with company ID. company = Company() + cla.log.info(f'{fn} - loading company details for: {request_info}') company.load(company_id) - cla.log.info(f'Loaded company details for: {request_info}') + cla.log.info(f'{fn} - loaded company details for: {request_info}') # user has already been checked from check_and_prepare_employee_signature. Load user with user ID. user = User() @@ -534,9 +692,10 @@ def request_employee_signature(self, project_id, company_id, user_id, return_url # Get project's latest corporate document to get major/minor version numbers. last_document = project.get_latest_corporate_document() - cla.log.info(f'Loaded last project document details for: {request_info}') + cla.log.info(f'{fn} - loaded the current cla document document details for: {request_info}') # return_url may still be empty at this point - the console will deal with it + cla.log.info(f'{fn} - creating a new signature document for: {request_info}') new_signature = Signature(signature_id=str(uuid.uuid4()), signature_project_id=project_id, signature_document_minor_version=last_document.get_document_minor_version(), @@ -549,25 +708,32 @@ def request_employee_signature(self, project_id, company_id, user_id, return_url signature_approved=True, signature_return_url=return_url, signature_user_ccla_company_id=company_id) - cla.log.info(f'Created new signature document for: {request_info} - signature: {new_signature}') + cla.log.info(f'{fn} - created new signature document for: {request_info} - signature: {new_signature}') # Set signature ACL - new_signature.set_signature_acl(f'github:{user.get_user_github_id()}') + if return_url_type.lower() == "github": + acl_value = f'github:{user.get_user_github_id()}' + elif return_url_type.lower() == "gitlab": + acl_value = f'gitlab:{user.get_user_gitlab_id()}' + cla.log.info(f'{fn} - assigning signature acl with value: {acl_value} for: {request_info}') + new_signature.set_signature_acl(acl_value) # Save signature - new_signature.save() - cla.log.info(f'Set and saved signature for: {request_info}') - event_data = (f'The user {user.get_user_name()} acknowledged the CLA affiliation for ' + # new_signature.save() + self._save_employee_signature(new_signature) + cla.log.info(f'{fn} - saved signature for: {request_info}') + event_data = (f'The user {user.get_user_name()} acknowledged the CLA employee affiliation for ' f'company {company.get_company_name()} with ID {company.get_company_id()}, ' - f'project {project.get_project_name()} with ID {project.get_project_id()}.') - event_summary = (f'The user {user.get_user_name()} acknowledged the CLA affiliation for ' + f'cla group {project.get_project_name()} with ID {project.get_project_id()}.') + event_summary = (f'The user {user.get_user_name()} acknowledged the CLA employee affiliation for ' f'company {company.get_company_name()} and ' - f'project {project.get_project_name()}.') + f'cla group {project.get_project_name()}.') Event.create_event( event_type=EventType.EmployeeSignatureCreated, event_company_id=company_id, - event_project_id=project_id, + event_cla_group_id=project_id, event_user_id=user_id, + event_user_name=user.get_user_name() if user else None, event_data=event_data, event_summary=event_summary, contains_pii=True, @@ -576,33 +742,79 @@ def request_employee_signature(self, project_id, company_id, user_id, return_url # If the project does not require an ICLA to be signed, update the pull request and remove the active # signature metadata. if not project.get_project_ccla_requires_icla_signature(): - cla.log.info('Project does not require ICLA signature from the employee - updating PR') - github_repository_id = signature_metadata['repository_id'] - change_request_id = signature_metadata['pull_request_id'] + cla.log.info(f'{fn} - cla group does not require a separate ICLA signature from the employee - updating PR') - # Get repository - installation_id = cla.utils.get_installation_id_from_github_repository(github_repository_id) - if installation_id is None: - return {'errors': {'github_repository_id': 'The given github repository ID does not exist. '}} + if return_url_type.lower() == "github": + # Get repository + github_repository_id = signature_metadata['repository_id'] + change_request_id = signature_metadata['pull_request_id'] + installation_id = cla.utils.get_installation_id_from_github_repository(github_repository_id) + if installation_id is None: + return {'errors': {'github_repository_id': 'The given github repository ID does not exist. '}} + + update_repository_provider(installation_id, github_repository_id, change_request_id) + + elif return_url_type.lower() == "gitlab": + gitlab_repository_id = int(signature_metadata['repository_id']) + merge_request_id = int(signature_metadata['merge_request_id']) + organization_id = cla.utils.get_organization_id_from_gitlab_repository(gitlab_repository_id) + self._update_gitlab_mr(organization_id, gitlab_repository_id, merge_request_id) + + if organization_id is None: + return {'errors': {'gitlab_repository_id': 'The given github repository ID does not exist. '}} - update_repository_provider(installation_id, github_repository_id, change_request_id) cla.utils.delete_active_signature_metadata(user_id) else: - cla.log.info('Project requires ICLA signature from employee - PR has been left unchanged') + cla.log.info(f'{fn} - cla group requires ICLA signature from employee - PR has been left unchanged') - cla.log.info(f'Returning new signature for: {request_info} - signature: {new_signature}') + cla.log.info(f'{fn} - returning new signature for: {request_info} - signature: {new_signature}') return new_signature.to_dict() + + def _save_employee_signature(self,signature): + cla.log.info(f'Saving signature record (boto3): {signature}') + item = { + 'signature_id' : {'S': signature.get_signature_id()}, + 'signature_project_id': {'S': signature.get_signature_project_id()}, + 'signature_document_minor_version': {'N': str(signature.get_signature_document_minor_version())}, + 'signature_document_major_version': {'N': str(signature.get_signature_document_major_version())}, + 'signature_reference_id': {'S': signature.get_signature_reference_id()}, + 'signature_reference_type': {'S': signature.get_signature_reference_type()}, + 'signature_type': {'S': signature.get_signature_type()}, + 'signature_signed': {'BOOL': signature.get_signature_signed()}, + 'signature_approved': {'BOOL': signature.get_signature_approved()}, + 'signature_acl': {'SS': list(signature.get_signature_acl())}, + 'signature_user_ccla_company_id': {'S': signature.get_signature_user_ccla_company_id()}, + 'date_modified': {'S': datetime.now().isoformat()}, + 'date_created': {'S': datetime.now().isoformat()} + } + + if signature.get_signature_return_url() is not None: + item['signature_return_url'] = {'S': signature.get_signature_return_url()} + + if signature.get_signature_reference_name() is not None: + item['signature_reference_name'] = {'S': signature.get_signature_reference_name()} + + try: + self.dynamo_client.put_item(TableName=signature_table, Item=item) + except Exception as e: + cla.log.error(f'Error while saving signature record (boto3): {e}') + raise e + + cla.log.info(f'Saved signature record (boto3): {signature}') + + return signature.get_signature_id() def request_employee_signature_gerrit(self, project_id, company_id, user_id, return_url=None): - request_info = f'project: {project_id}, company: {company_id}, user: {user_id} with return_url: {return_url}' - cla.log.info(f'Processing request_employee_signature_gerrit request with {request_info}') + fn = 'docusign_models.request_employee_signature_gerrit' + request_info = f'cla group: {project_id}, company: {company_id}, user: {user_id} with return_url: {return_url}' + cla.log.info(f'{fn} - processing request_employee_signature_gerrit request with {request_info}') check_and_prepare_signature = self.check_and_prepare_employee_signature(project_id, company_id, user_id) # Check if there are any errors while preparing the signature. if 'errors' in check_and_prepare_signature: - cla.log.warning(f'Error in request_employee_signature_gerrit with: {request_info} - ' + cla.log.warning(f'{fn} - error in request_employee_signature_gerrit with: {request_info} - ' f'signatures: {check_and_prepare_signature}') return check_and_prepare_signature @@ -611,27 +823,31 @@ def request_employee_signature_gerrit(self, project_id, company_id, user_id, ret company_id=company_id, project_id=project_id, user_id=user_id) # Return existing signature if employee has signed it if employee_signature is not None: - cla.log.info(f'Employee has signed for company: {company_id}, ' + cla.log.info(f'{fn} - employee has signed for company: {company_id}, ' f'request_info: {request_info} - signature: {employee_signature}') return employee_signature.to_dict() - cla.log.info(f'Employee has NOT signed it for: {request_info}') + cla.log.info(f'{fn} - employee has NOT previously acknowledged their company affiliation for : {request_info}') # Retrieve Gerrits by Project reference ID try: + cla.log.info(f'{fn} - loading gerrits for: {request_info}') gerrits = Gerrit().get_gerrit_by_project_id(project_id) except DoesNotExist as err: - cla.log.error(f'Cannot load Gerrit instance for: {request_info}') + cla.log.error(f'{fn} - cannot load Gerrit instance for: {request_info}') return {'errors': {'missing_gerrit': str(err)}} # project has already been checked from check_and_prepare_employee_signature. Load project with project ID. project = Project() + cla.log.info(f'{fn} - loading cla group for: {request_info}') project.load(project_id) - cla.log.info(f'Loaded project for: {request_info}') + cla.log.info(f'{fn} - loaded cla group for: {request_info}') + # company has already been checked from check_and_prepare_employee_signature. Load company with company ID. company = Company() + cla.log.info(f'{fn} - loading company details for: {request_info}') company.load(company_id) - cla.log.info(f'Loaded company details for: {request_info}') + cla.log.info(f'{fn} - loaded company details for: {request_info}') # user has already been checked from check_and_prepare_employee_signature. Load user with user ID. user = User() @@ -656,8 +872,13 @@ def request_employee_signature_gerrit(self, project_id, company_id, user_id, ret new_signature.set_signature_acl(user.get_lf_username()) # Save signature before adding user to the LDAP Group. - new_signature.save() - cla.log.info(f'Set and saved signature for: {request_info}') + cla.log.debug(f'{fn} - saving signature...{new_signature.to_dict()}') + try: + self._save_employee_signature(new_signature) + except Exception as ex: + cla.log.error(f'{fn} - unable to save signature error: {ex}') + return + cla.log.info(f'{fn} - saved signature for: {request_info}') event_data = (f'The user {user.get_user_name()} acknowledged the CLA company affiliation for ' f'company {company.get_company_name()} with ID {company.get_company_id()}, ' f'project {project.get_project_name()} with ID {project.get_project_id()}.') @@ -667,8 +888,9 @@ def request_employee_signature_gerrit(self, project_id, company_id, user_id, ret Event.create_event( event_type=EventType.EmployeeSignatureCreated, event_company_id=company_id, - event_project_id=project_id, + event_cla_group_id=project_id, event_user_id=user_id, + event_user_name=user.get_user_name() if user else None, event_data=event_data, event_summary=event_summary, contains_pii=True, @@ -680,9 +902,10 @@ def request_employee_signature_gerrit(self, project_id, company_id, user_id, ret group_id = gerrit.get_group_id_ccla() # Add the user to the LDAP Group try: + cla.log.debug(f'{fn} - adding user to group: {group_id}') lf_group.add_user_to_group(group_id, user.get_lf_username()) except Exception as e: - cla.log.error('Failed in adding user to the LDAP group.{} - {}'.format(e, request_info)) + cla.log.error(f'{fn} - failed in adding user to the LDAP group.{e} - {request_info}') return return new_signature.to_dict() @@ -694,6 +917,7 @@ def _generate_individual_signature_callback_url_gerrit(self, user_id): """ return os.path.join(api_base_url, 'v2/signed/gerrit/individual', str(user_id)) + def _get_corporate_signature_callback_url(self, project_id, company_id): """ Helper function to get the callback_url of a CCLA signature. @@ -780,25 +1004,23 @@ def handle_signing_new_corporate_signature(self, signature, project, company, us return response_model def request_corporate_signature(self, auth_user: object, - project_id: object, - company_id: object, + project_id: str, + company_id: str, signing_entity_name: str = None, - send_as_email: object = False, - signatory_name: object = None, - signatory_email: object = None, - return_url_type: object = None, - return_url: object = None) -> object: + send_as_email: bool = False, + signatory_name: str = None, + signatory_email: str = None, + return_url_type: str = None, + return_url: str = None) -> object: fn = 'models.docusign_models.request_corporate_signature' cla.log.debug(f'{fn} - ' f'project id: {project_id}, ' f'company id: {company_id}, ' f'signing entity name: {signing_entity_name}, ' - f'send email: {send_as_email}', + f'send email: {send_as_email}, ' f'signatory name: {signatory_name}, ' - f'signatory email: {signatory_email} ' - f'return url type: {return_url_type}', - f'return url: {return_url}', + f'signatory email: {signatory_email}, ' ) # Auth user is the currently logged in user - the user who started the signing process @@ -811,7 +1033,7 @@ def request_corporate_signature(self, auth_user: object, return {'errors': {'company_id': 'request_corporate_signature - company_id is empty'}} if auth_user is None: - return {'errors': {'user_error': 'request_corporate_signature - auth_user is empty'}} + return {'errors': {'user_error': 'request_corporate_signature - auth_user object is empty'}} if auth_user.username is None: return {'errors': {'user_error': 'request_corporate_signature - auth_user.username is empty'}} @@ -820,7 +1042,8 @@ def request_corporate_signature(self, auth_user: object, cla.log.debug(f'{fn} - loading user {auth_user.username}') users_list = User().get_user_by_username(auth_user.username) if users_list is None: - cla.log.debug(f'{fn} - unable to load auth_user by username: {auth_user.username} from the EasyCLA database.') + cla.log.debug(f'{fn} - unable to load auth_user by username: {auth_user.username} ' + 'from the EasyCLA database.') # Lookup user in the platform user service... us = UserService # If found, create user record in our EasyCLA database @@ -831,7 +1054,8 @@ def request_corporate_signature(self, auth_user: object, 'Returning an error response') return {'errors': {'user_error': 'user does not exist'}} if len(platform_users) > 1: - cla.log.warning(f'{fn} - more than one user with same username: {auth_user.username} - using first record.') + cla.log.warning(f'{fn} - more than one user with same username: {auth_user.username} - ' + 'using first record.') # Grab the first user from the list - should only be one that matches the search query parameters platform_user = platform_users[0] @@ -874,43 +1098,49 @@ def request_corporate_signature(self, auth_user: object, # unlikely we'll have more than one cla_manager_user = users_list[0] - # Add some defensive checks to ensure the Name and Email are set for the CLA Manager + # Add some defensive checks to ensure the Name and Email are set for the CLA Manager - lookup the values + # from the platform user service - use this as the source of truth us = UserService cla.log.debug(f'{fn} - Loading user by username: {auth_user.username} from the platform user service...') platform_users = us.get_users_by_username(auth_user.username) - if platform_users is None: - cla.log.warning(f'{fn} - Unable to load auth_user by username: {auth_user.username}. ' - 'Returning an error response') - return {'errors': {'user_error': 'user does not exist'}} - platform_user = platform_users[0] - - if cla_manager_user.get_user_name() is None: - # Lookup user in the platform user service... - cla.log.warning(f'{fn} - Loaded CLA Manager by username: {auth_user.username}, but ' - 'the user_name is missing from profile - required for DocuSign.') - user_name = platform_user.get('Name', None) - if user_name: - cla.log.debug(f'{fn} - user_name: {user_name} update for cla_manager : {auth_user.username}...') - cla_manager_user.set_user_name(user_name) - cla_manager_user.save() - else: - return {'errors': {'user_error': 'user does not have user_name'}} + if platform_users: + platform_user = platform_users[0] - if cla_manager_user.get_user_email() is None: - cla.log.warning(f'{fn} - Loaded CLA Manager by username: {auth_user.username}, but ' - 'the user email is missing from profile - required for DocuSign.') - # Add the emails - platform_user_emails = platform_user.get('Emails', None) - if len(platform_user_emails) > 0: - email_list = [] - for platform_email in platform_user_emails: - email_list.append(platform_email['EmailAddress']) - if platform_email['IsPrimary']: - cla_manager_user.set_lf_email(platform_email['EmailAddress']) - cla_manager_user.set_user_emails(email_list) - cla_manager_user.save() - else: - return {'errors': {'user_error': 'user does not have an email'}} + if cla_manager_user.get_user_name() is None: + # Lookup user in the platform user service... + cla.log.warning(f'{fn} - Loaded CLA Manager by username: {auth_user.username}, but ' + 'the user_name is missing from profile - required for DocuSign.') + user_name = platform_user.get('Name', None) + if user_name: + if cla_manager_user.get_user_name() != user_name: + cla.log.debug(f'{fn} - user_name: {user_name} update for cla_manager : {auth_user.username}...') + cla_manager_user.set_user_name(user_name) + cla_manager_user.save() + else: + cla.log.debug(f'{fn} - user_name values match - no need to update the local record') + else: + cla.log.warning(f'{fn} - Unable to locate the user\'s name from the platform user service model. ' + 'Unable to update the local user record.') + + if cla_manager_user.get_user_email() is None: + cla.log.warning(f'{fn} - Loaded CLA Manager by username: {auth_user.username}, but ' + 'the user email is missing from profile - required for DocuSign.') + # Add the emails + platform_user_emails = platform_user.get('Emails', None) + if len(platform_user_emails) > 0: + email_list = [] + for platform_email in platform_user_emails: + email_list.append(platform_email['EmailAddress']) + if platform_email['IsPrimary']: + cla_manager_user.set_lf_email(platform_email['EmailAddress']) + cla_manager_user.set_user_emails(email_list) + cla_manager_user.save() + else: + cla.log.warning(f'{fn} - Unable to locate the user\'s email from the platform user service model. ' + 'Unable to update the local user record.') + else: + cla.log.warning(f'{fn} - Unable to load auth_user from the platform user service ' + f'by username: {auth_user.username}. Unable to update our local user record.') cla.log.debug(f'{fn} - Loaded user {cla_manager_user} - this is our CLA Manager') # Ensure the project exists @@ -931,11 +1161,16 @@ def request_corporate_signature(self, auth_user: object, company.load(str(company_id)) cla.log.debug(f'{fn} - Loaded company {company}') + if signing_entity_name is None: + if company.get_signing_entity_name() is None: + signing_entity_name = company.get_company_name() + else: + signing_entity_name = company.get_signing_entity_name() + # Should be the same values...what do we do if they do not match? if company.get_signing_entity_name() != signing_entity_name: cla.log.warning(f'{fn} - signing entity name provided: {signing_entity_name} ' f'does not match the DB company record: {company.get_signing_entity_name()}') - except DoesNotExist as err: cla.log.warning(f'{fn} - Unable to load company by id: {company_id}. ' 'Returning an error response') @@ -980,7 +1215,8 @@ def request_corporate_signature(self, auth_user: object, signatory_name=signatory_name, signatory_email=signatory_email, send_as_email=send_as_email, return_url_type=return_url_type, return_url=return_url) - cla.log.debug(f'{fn} - Previous unsigned CCLA signatures on file for project: {project_id}, company: {company_id}') + cla.log.debug(f'{fn} - Previous unsigned CCLA signatures on file for project: {project_id},' + f'company: {company_id}') # TODO: should I delete all but one? return self.handle_signing_new_corporate_signature( signature=signatures[0], project=project, company=company, user=cla_manager_user, @@ -995,9 +1231,10 @@ def populate_sign_url(self, signature, callback_url=None, default_values: Optional[Dict[str, Any]] = None, preferred_email: str = None): # pylint: disable=too-many-locals + fn = 'populate_sign_url' sig_type = signature.get_signature_reference_type() - cla.log.debug(f'populate_sign_url - Populating sign_url for signature {signature.get_signature_id()} ' + cla.log.debug(f'{fn} - Populating sign_url for signature {signature.get_signature_id()} ' f'using callback: {callback_url} ' f'with authority_or_signatory_name {authority_or_signatory_name} ' f'with authority_or_signatory_email {authority_or_signatory_email} ' @@ -1015,35 +1252,35 @@ def populate_sign_url(self, signature, callback_url=None, user_signature_name = 'Unknown' user_signature_email = 'Unknown' - cla.log.debug(f'populate_sign_url - {sig_type} - processing signing request...') + cla.log.debug(f'{fn} - {sig_type} - processing signing request...') if sig_type == 'company': # For CCLA - use provided CLA Manager information user_signature_name = cla_manager_name user_signature_email = cla_manager_email - cla.log.debug(f'populate_sign_url - {sig_type} - user_signature name/email will be CLA Manager name/info: ' + cla.log.debug(f'{fn} - {sig_type} - user_signature name/email will be CLA Manager name/info: ' f'{user_signature_name} / {user_signature_email}...') try: # Grab the company id from the signature - cla.log.debug('populate_sign_url - CCLA - ' + cla.log.debug('{fn} - CCLA - ' f'Loading company id: {signature.get_signature_reference_id()}') company.load(signature.get_signature_reference_id()) - cla.log.debug(f'populate_sign_url - {sig_type} - loaded company: {company}') + cla.log.debug(f'{fn} - {sig_type} - loaded company: {company}') except DoesNotExist: - cla.log.warning(f'populate_sign_url - {sig_type} - ' + cla.log.warning(f'{fn} - {sig_type} - ' 'No CLA manager associated with this company - can not sign CCLA') return except Exception as e: - cla.log.warning(f'populate_sign_url - {sig_type} - No CLA manager lookup error: {e}') + cla.log.warning(f'{fn} - {sig_type} - No CLA manager lookup error: {e}') return elif sig_type == 'user': if not send_as_email: try: - cla.log.debug(f'populate_sign_url - {sig_type} - ' + cla.log.debug(f'{fn} - {sig_type} - ' f'loading user by reference id: {signature.get_signature_reference_id()}') user.load(signature.get_signature_reference_id()) - cla.log.debug(f'populate_sign_url - {sig_type} - loaded user by ' + cla.log.debug(f'{fn} - {sig_type} - loaded user by ' f'id: {user.get_user_id()}, ' f'name: {user.get_user_name()}, ' f'email: {user.get_user_email()}') @@ -1052,50 +1289,50 @@ def populate_sign_url(self, signature, callback_url=None, if not user.get_user_email() is None: user_signature_email = user.get_user_email() except DoesNotExist: - cla.log.warning(f'populate_sign_url - {sig_type} - no user associated with this signature ' + cla.log.warning(f'{fn} - {sig_type} - no user associated with this signature ' f'id: {signature.get_signature_reference_id()} - can not sign ICLA') return except Exception as e: - cla.log.warning(f'populate_sign_url - {sig_type} - no user associated with this signature - ' + cla.log.warning(f'{fn} - {sig_type} - no user associated with this signature - ' f'id: {signature.get_signature_reference_id()}, ' f'error: {e}') return cla.log.debug( - f'populate_sign_url - {sig_type} - user_signature name/email will be user from signature: ' + f'{fn} - {sig_type} - user_signature name/email will be user from signature: ' f'{user_signature_name} / {user_signature_email}...') else: - cla.log.warning(f'populate_sign_url - unsupported signature type: {sig_type}') + cla.log.warning(f'{fn} - unsupported signature type: {sig_type}') return # Fetch the document template to sign. project = Project() - cla.log.debug(f'populate_sign_url - {sig_type} - ' + cla.log.debug(f'{fn} - {sig_type} - ' f'loading project by id: {signature.get_signature_project_id()}') project.load(signature.get_signature_project_id()) - cla.log.debug(f'populate_sign_url - {sig_type} - ' + cla.log.debug(f'{fn} - {sig_type} - ' f'loaded project by id: {signature.get_signature_project_id()} - ' f'project: {project}') # Load the appropriate document if sig_type == 'company': - cla.log.debug(f'populate_sign_url - {sig_type} - loading project_corporate_document...') + cla.log.debug(f'{fn} - {sig_type} - loading project_corporate_document...') document = project.get_project_corporate_document() if document is None: - cla.log.error(f'populate_sign_url - {sig_type} - Could not get sign url for project: {project}. ' + cla.log.error(f'{fn} - {sig_type} - Could not get sign url for project: {project}. ' 'Project has no corporate CLA document set. Returning...') return - cla.log.debug(f'populate_sign_url - {sig_type} - loaded project_corporate_document...') + cla.log.debug(f'{fn} - {sig_type} - loaded project_corporate_document...') else: # sig_type == 'user' - cla.log.debug(f'populate_sign_url - {sig_type} - loading project_individual_document...') + cla.log.debug(f'{fn} - {sig_type} - loading project_individual_document...') document = project.get_project_individual_document() if document is None: - cla.log.error(f'populate_sign_url - {sig_type} - Could not get sign url for project: {project}. ' + cla.log.error(f'{fn} - {sig_type} - Could not get sign url for project: {project}. ' 'Project has no individual CLA document set. Returning...') return cla.log.debug(f'populate_sign_url - {sig_type} - loaded project_individual_document...') - # Void the existing envelope to prevent multiple envelopes pending for a signer. + # Void the existing envelope to prevent multiple envelopes pending for a signer. envelope_id = signature.get_signature_envelope_id() if envelope_id is not None: try: @@ -1105,7 +1342,7 @@ def populate_sign_url(self, signature, callback_url=None, cla.log.debug(message) self.client.void_envelope(envelope_id, message) except Exception as e: - cla.log.warning(f'populate_sign_url - {sig_type} - DocuSign error while voiding the envelope - ' + cla.log.warning(f'{fn} - {sig_type} - DocuSign error while voiding the envelope - ' f'regardless, continuing on..., error: {e}') # Not sure what should be put in as documentId. @@ -1113,7 +1350,7 @@ def populate_sign_url(self, signature, callback_url=None, tabs = get_docusign_tabs_from_document(document, document_id, default_values=default_values) if send_as_email: - cla.log.warning(f'populate_sign_url - {sig_type} - assigning signatory name/email: ' + cla.log.warning(f'{fn} - {sig_type} - assigning signatory name/email: ' f'{authority_or_signatory_name} / {authority_or_signatory_email}') # Sending email to authority signatory_email = authority_or_signatory_email @@ -1121,18 +1358,26 @@ def populate_sign_url(self, signature, callback_url=None, # Not assigning a clientUserId sends an email. project_name = project.get_project_name() + cla_group_name = project_name company_name = company.get_company_name() + project_cla_group = get_project_cla_group_instance() + project_cla_groups = project_cla_group.get_by_cla_group_id(project.get_project_id()) + project_names = [p.get_project_name() for p in project_cla_groups] + if not project_names: + project_names = [project_name] - cla.log.debug(f'populate_sign_url - {sig_type} - sending document as email with ' + cla.log.debug(f'{fn} - {sig_type} - sending document as email with ' f'name: {signatory_name}, email: {signatory_email} ' f'project name: {project_name}, company: {company_name}') - email_subject = f'EasyCLA: CLA Signature Request for {project_name}' - email_body = f'Hello {signatory_name},
' - email_body += f'
This is a notification email from EasyCLA regarding the project {project_name}. {cla_manager_name} has designated you as being an authorized signatory for {company_name}. In order for employees of your company to contribute to the open source project {project_name}, they must do so under a Contributor License Agreement signed by someone with authority to sign on behalf of your company.
' - email_body += f'After you sign, {cla_manager_name} (as the initial CLA Manager for your company) will be able to maintain the list of specific employees authorized to contribute to the project under this signed CLA.
' - email_body += f'If you are authorized to sign on your company’s behalf, and if you approve {cla_manager_name} as your initial CLA Manager for {project_name}, please click the link below to review and sign the CLA.If you have questions, or if you are not an authorized signatory of this company, please contact the requester at {cla_manager_email}.
' - email_body = append_email_help_sign_off_content(email_body, project.get_version()) + email_subject, email_body = cla_signatory_email_content( + ClaSignatoryEmailParams(cla_group_name=cla_group_name, + signatory_name=signatory_name, + cla_manager_name=cla_manager_name, + cla_manager_email=cla_manager_email, + company_name=company_name, + project_version=project.get_version(), + 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 = pydocusign.Signer(email=signatory_email, @@ -1185,7 +1430,7 @@ def populate_sign_url(self, signature, callback_url=None, pdf = io.BytesIO(content) doc_name = document.get_document_name() - cla.log.debug(f'populate_sign_url - {sig_type} - docusign document ' + cla.log.debug(f'{fn} - {sig_type} - docusign document ' f'name: {doc_name}, id: {document_id}, content type: {content_type}') document = pydocusign.Document(name=doc_name, documentId=document_id, data=pdf) @@ -1228,11 +1473,13 @@ def populate_sign_url(self, signature, callback_url=None, signature.set_signature_sign_url(sign_url) # Save Envelope ID in signature. - cla.log.debug(f'populate_sign_url - {sig_type} - saving signature to database...') + cla.log.debug(f'{fn} - {sig_type} - saving signature to database...') 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') + def signed_individual_callback(self, content, installation_id, github_repository_id, change_request_id): """ Will be called on ICLA signature callback, but also when a document has been @@ -1301,6 +1548,7 @@ def signed_individual_callback(self, content, installation_id, github_repository # Log the event try: # Load the Project by ID and send audit event + cla.log.debug(f'{fn} - creating an event log entry for event_type: {EventType.IndividualSignatureSigned}') project = Project() project.load(signature.get_signature_project_id()) event_data = (f'The user {user.get_user_name()} signed an individual CLA for ' @@ -1309,13 +1557,15 @@ def signed_individual_callback(self, content, installation_id, github_repository f'project {project.get_project_name()} with project ID: {project.get_project_id()}.') Event.create_event( event_type=EventType.IndividualSignatureSigned, - event_project_id=signature.get_signature_project_id(), + event_cla_group_id=signature.get_signature_project_id(), event_company_id=None, event_user_id=signature.get_signature_reference_id(), + event_user_name=user.get_user_name() if user else None, event_data=event_data, event_summary=event_summary, contains_pii=False, ) + cla.log.debug(f'{fn} - created an event log entry for event_type: {EventType.IndividualSignatureSigned}') except DoesNotExist as err: msg = (f'{fn} - unable to load project by CLA Group ID: {signature.get_signature_project_id()}, ' f'unable to send audit event, error: {err}') @@ -1362,9 +1612,10 @@ def signed_individual_callback_gerrit(self, content, user_id): f'project {project.get_project_name()} with project ID: {project.get_project_id()}.') Event.create_event( event_type=EventType.IndividualSignatureSigned, - event_project_id=signature.get_signature_project_id(), + event_cla_group_id=signature.get_signature_project_id(), event_company_id=None, event_user_id=user.get_user_id(), + event_user_name=user.get_user_name(), event_data=event_data, event_summary=event_summary, contains_pii=False, @@ -1405,6 +1656,109 @@ def signed_individual_callback_gerrit(self, content, user_id): self.send_to_s3(document_data, project_id, signature_id, 'icla', user_id) cla.log.debug(f'{fn} - uploaded ICLA document to s3') + def _update_gitlab_mr(self, organization_id: str , gitlab_repository_id: int, merge_request_id: int) -> None: + """ + Helper function that updates mr upon a successful signing + param organization_id: Gitlab group id + rtype organization_id: int + param gitlab_repository_id: Gitlab repository + rtype: int + param merge_request_id: Gitlab MR + rtype: int + """ + fn = 'models.docusign_models._update_gitlab_mr' + try: + headers = { + 'Content-type': 'application/json', + 'Accept': 'application/json' + } + url = f'{cla.config.PLATFORM_GATEWAY_URL}/cla-service/v4/gitlab/trigger' + payload = { + "gitlab_external_repository_id": gitlab_repository_id, + "gitlab_mr_id": merge_request_id, + "gitlab_organization_id": organization_id + } + requests.post(url, data=json.dumps(payload), headers=headers) + cla.log.debug(f'{fn} - Updating GitLab MR with payload: {payload}') + except requests.exceptions.HTTPError as err: + msg = f'{fn} - Unable to update GitLab MR: {merge_request_id}, error: {err}' + cla.log.warning(msg) + + def signed_individual_callback_gitlab(self, content, user_id, organization_id, gitlab_repository_id, merge_request_id): + fn = 'models.docusign_models.signed_individual_callback_gitlab' + cla.log.debug(f'{fn} - Docusign GitLab ICLA signed callback POST data: {content}') + tree = ET.fromstring(content) + # Get envelope ID. + envelope_id = tree.find('.//' + self.TAGS['envelope_id']).text + # Assume only one signature per signature. + signature_id = tree.find('.//' + self.TAGS['client_user_id']).text + signature = cla.utils.get_signature_instance() + try: + signature.load(signature_id) + except DoesNotExist: + cla.log.error(f'{fn} - DocuSign GitLab ICLA callback returned signed info ' + f'on invalid signature: {content}') + return + # Iterate through recipients and update the signature signature status if changed. + elem = tree.find('.//' + self.TAGS['recipient_statuses'] + + '/' + self.TAGS['recipient_status']) + status = elem.find(self.TAGS['status']).text + if status == 'Completed' and not signature.get_signature_signed(): + cla.log.info(f'{fn} - ICLA signature signed ({signature_id}) - notifying repository service provider') + # Get User + user = cla.utils.get_user_instance() + user.load(user_id) + + cla.log.debug(f'{fn} - updating signature in database - setting signed=true...') + signature.set_signature_signed(True) + populate_signature_from_icla_callback(content, tree, signature) + signature.save() + + #Update repository provider (GitLab) + self._update_gitlab_mr(organization_id, gitlab_repository_id, merge_request_id) + + # Load the Project by ID and send audit event + project = Project() + try: + project.load(signature.get_signature_project_id()) + event_data = (f'The user {user.get_user_name()} signed an individual CLA for ' + f'project {project.get_project_name()}.') + event_summary = (f'The user {user.get_user_name()} signed an individual CLA for ' + f'project {project.get_project_name()} with project ID: {project.get_project_id()}.') + Event.create_event( + event_type=EventType.IndividualSignatureSigned, + event_cla_group_id=signature.get_signature_project_id(), + event_company_id=None, + event_user_id=user.get_user_id(), + event_user_name=user.get_user_name(), + event_data=event_data, + event_summary=event_summary, + contains_pii=False, + ) + except DoesNotExist as err: + msg = (f'{fn} - unable to load project by CLA Group ID: {signature.get_signature_project_id()}, ' + f'unable to send audit event, error: {err}') + cla.log.warning(msg) + return + + # Remove the active signature metadata. + cla.utils.delete_active_signature_metadata(user.get_user_id()) + + # Get signed document + document_data = self.get_signed_document(envelope_id, user) + # Send email with signed document. + self.send_signed_document(signature, document_data, user, icla=True) + + # Verify user id exist for saving on storage + if user_id is None: + cla.log.warning(f'{fn} - missing user_id on ICLA for saving signed file on s3 storage') + raise SigningError('Missing user_id on ICLA for saving signed file on s3 storage.') + + # Store document on S3 + project_id = signature.get_signature_project_id() + self.send_to_s3(document_data, project_id, signature_id, 'icla', user_id) + cla.log.debug(f'{fn} - uploaded ICLA document to s3') + def signed_corporate_callback(self, content, project_id, company_id): """ Will be called on CCLA signature callback, but also when a document has been @@ -1451,7 +1805,7 @@ def signed_corporate_callback(self, content, project_id, company_id): cla.log.warning(msg) return {'errors': {'error': msg}} else: - # If client_user_id is None, the callback came from the email that finished signing. + # If client_user_id is None, the callback came from the email that finished signing. # Retrieve the latest signature with projectId and CompanyId. signature = company.get_latest_signature(str(project_id)) signature_id = signature.get_signature_id() @@ -1507,33 +1861,36 @@ def signed_corporate_callback(self, content, project_id, company_id): # Update our event/activity log if signature.get_signature_reference_type() == 'user': event_data = (f'The user {user.get_user_name()} signed an individual CLA for ' - f'project {project.get_project_name()}.') + f'the project {project.get_project_name()}.') event_summary = (f'The user {user.get_user_name()} signed an individual CLA for ' - f'project {project.get_project_name()} with project ID: {project.get_project_id()}.') + f'the project {project.get_project_name()} with ' + f'the project ID: {project.get_project_id()}.') Event.create_event( event_type=EventType.IndividualSignatureSigned, - event_project_id=project_id, + event_cla_group_id=project_id, event_company_id=None, event_user_id=user.get_user_id(), + event_user_name=user.get_user_name(), event_data=event_data, event_summary=event_summary, contains_pii=False, ) elif signature.get_signature_reference_type() == 'company': - event_data = (f'Corporate signature ' - f'signed for project {project.get_project_name()} ' + event_data = (f'A corporate signature ' + f'was signed for project {project.get_project_name()} ' f'and company {company.get_company_name()} ' - f'by user {user.get_user_name()}, ' + f'by {signature.get_signatory_name()}, ' f'params: {param_str}') event_summary = (f'A corporate signature ' - f'was signed for project {project.get_project_name()} ' - f'and company {company.get_company_name()} ' - f'by user {user.get_user_name()}.') + f'was signed for the project {project.get_project_name()} ' + f'and the company {company.get_company_name()} ' + f'by {signature.get_signatory_name()}.') Event.create_event( event_type=EventType.CompanySignatureSigned, - event_project_id=project_id, + event_cla_group_id=project_id, event_company_id=company.get_company_id(), event_user_id=user.get_user_id(), + event_user_name=signature.get_signatory_name(), event_data=event_data, event_summary=event_summary, contains_pii=False, @@ -1545,7 +1902,7 @@ def signed_corporate_callback(self, content, project_id, company_id): except DoesNotExist: gerrits = [] - # Get LF user name. + # Get LF user name. lf_username = user.get_lf_username() for gerrit in gerrits: # Get Gerrit Group ID @@ -1630,31 +1987,11 @@ def send_signed_document(self, signature, document_data, user, icla=True): project = Project() project.load(signature.get_signature_project_id()) except DoesNotExist as err: - cla.log.warning(f'{fn} - unable to load project by id: {project.get_project_id()} - ' + cla.log.warning(f'{fn} - unable to load project by id: {signature.get_signature_project_id()} - ' 'unable to send email to user') return - # subject = 'EasyCLA: Signed Document' - # body = 'Thank you for signing the CLA! Your signed document is attached to this email.' - if icla: - pdf_link = (f'{cla.conf["API_BASE_URL"]}/v3/' - f'signatures/{project.get_project_id()}/' - f'{user.get_user_id()}/icla/pdf') - else: - pdf_link = (f'{cla.conf["API_BASE_URL"]}/v3/' - f'signatures/{project.get_project_id()}/' - f'{signature.get_signature_reference_id()}/ccla/pdf') - subject = f'EasyCLA: CLA Signature Signed for {project.get_project_name()}' - body = f''' -Hello {"Contributor" if icla else "CLA Signatory"},
-This is a notification email from EasyCLA regarding the project {project.get_project_name()}.
-Thank you for signing the CLA. You can download the PDF document - - from our website. -
- ''' - body = append_email_help_sign_off_content(body, project.get_version()) - + subject, body = document_signed_email_content(icla=icla, project=project, signature=signature, user=user) # Third, send the email. cla.log.debug(f'{fn} - sending signed CLA document to {recipient} with subject: {subject}') cla.utils.get_email_service().send(subject, body, recipient) @@ -1811,7 +2148,7 @@ def get_docusign_tabs_from_document(document: Document, } if tab.get_document_tab_anchor_string() is not None: - # Set only when anchor string exists + # 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() @@ -1832,11 +2169,19 @@ def get_docusign_tabs_from_document(document: Document, args['locked'] = False elif tab_type == 'text_optional': 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': tab_class = pydocusign.NumberTab elif tab_type == 'sign': tab_class = pydocusign.SignHereTab + elif tab_type == 'sign_optional': + 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': tab_class = pydocusign.DateSignedTab else: @@ -1925,6 +2270,19 @@ def populate_signature_from_ccla_callback(content: str, ccla_tree: ET, signature else: cla.log.warning(f'{fn} - unable to locate signatory_name field from docusign callback') + signing_entity_name_field = ccla_tree.find(".//*[@name='corporation_name']") + if signing_entity_name_field is not None: + signing_entity_name = signing_entity_name_field.find(DocuSign.TAGS['field_value']) + if signing_entity_name is not None: + signing_entity_name = signing_entity_name.text + cla.log.debug(f'{fn} - located signing_entity_name_field value in the docusign document callback - ' + f'setting user_docusign_name attribute: {signing_entity_name} value in the signature') + signature.set_signing_entity_name(signing_entity_name) + else: + cla.log.warning(f'{fn} - unable to extract signing_entity_name field_value from docusign callback') + else: + cla.log.warning(f'{fn} - unable to locate signing_entity_name field from docusign callback') + # seems the content could be bytes if hasattr(content, "decode"): content = content.decode("utf-8") @@ -2029,3 +2387,81 @@ def generate_manager_and_contributor_list(managers, contributors=None): lines = '\n'.join([str(line) for line in lines]) return lines + + +def document_signed_email_content(icla: bool, project: Project, signature: Signature, user: User) -> (str, str): + """ + document_signed_email_content prepares the email subject and body content for the signed documents + :return: + """ + # subject = 'EasyCLA: Signed Document' + # body = 'Thank you for signing the CLA! Your signed document is attached to this email.' + if icla: + pdf_link = (f'{cla.conf["API_BASE_URL"]}/v3/' + f'signatures/{project.get_project_id()}/' + f'{user.get_user_id()}/icla/pdf') + else: + pdf_link = (f'{cla.conf["API_BASE_URL"]}/v3/' + f'signatures/{project.get_project_id()}/' + f'{signature.get_signature_reference_id()}/ccla/pdf') + + corporate_url = get_corporate_url(project.get_version()) + + recipient_name = user.get_user_name() or user.get_lf_username() or None + # some defensive code + if not recipient_name: + if icla: + recipient_name = "Contributor" + else: + recipient_name = "CLA Manager" + + subject = f'EasyCLA: CLA Signed for {project.get_project_name()}' + + if icla: + body = f''' +Hello {recipient_name},
+This is a notification email from EasyCLA regarding the project {project.get_project_name()}.
+The CLA has now been signed. You can download the signed CLA as a PDF + + here. +
+ ''' + else: + body = f''' +Hello {recipient_name},
+This is a notification email from EasyCLA regarding the project {project.get_project_name()}.
+The CLA has now been signed. You can download the signed CLA as a PDF + + here, or from within the EasyCLA CLA Manager console . +
+ ''' + body = append_email_help_sign_off_content(body, project.get_version()) + return subject, body + + +@dataclass +class ClaSignatoryEmailParams: + cla_group_name: str + signatory_name: str + cla_manager_name: str + cla_manager_email: str + company_name: str + project_version: str + project_names: List[str] + + +def cla_signatory_email_content(params: ClaSignatoryEmailParams) -> (str, str): + """ + cla_signatory_email_content prepares the content for cla signatory + :param params: ClaSignatoryEmailParams + :return: + """ + project_names_list = ", ".join(params.project_names) + + email_subject = f'EasyCLA: CLA Signature Request for {params.cla_group_name}' + email_body = f'Hello {params.signatory_name},
' + email_body += f'
This is a notification email from EasyCLA regarding the project(s) {project_names_list} associated with the CLA Group {params.cla_group_name}. {params.cla_manager_name} has designated you as an authorized signatory for the organization {params.company_name}. In order for employees of your company to contribute to any of the above project(s), they must do so under a Contributor License Agreement signed by someone with authority n behalf of your company.
' + email_body += f'After you sign, {params.cla_manager_name} (as the initial CLA Manager for your company) will be able to maintain the list of specific employees authorized to contribute to the project(s) under this signed CLA.
' + email_body += f'If you are authorized to sign on your company’s behalf, and if you approve {params.cla_manager_name} as your initial CLA Manager, please review the document and sign the CLA. If you have questions, or if you are not an authorized signatory of this company, please contact the requester at {params.cla_manager_email}.
' + email_body = append_email_help_sign_off_content(email_body, params.project_version) + return email_subject, email_body \ No newline at end of file diff --git a/cla-backend/cla/models/dynamo_models.py b/cla-backend/cla/models/dynamo_models.py index a433712ba..b19680787 100644 --- a/cla-backend/cla/models/dynamo_models.py +++ b/cla-backend/cla/models/dynamo_models.py @@ -11,9 +11,11 @@ import re import time import uuid +from datetime import timezone from typing import Optional, List import dateutil.parser +from pynamodb import attributes from pynamodb.attributes import ( UTCDateTimeAttribute, UnicodeSetAttribute, @@ -22,7 +24,7 @@ NumberAttribute, ListAttribute, JSONAttribute, - MapAttribute, + MapAttribute, DESERIALIZE_CLASS_MAP, ) from pynamodb.expressions.condition import Condition from pynamodb.indexes import GlobalSecondaryIndex, AllProjection @@ -30,7 +32,10 @@ import cla from cla.models import model_interfaces, key_value_store_interface, DoesNotExist +from cla.models.event_types import EventType from cla.models.model_interfaces import User, Signature, ProjectCLAGroup, Repository, Gerrit +from cla.models.model_utils import is_uuidv4 +from cla.project_service import ProjectService stage = os.environ.get("STAGE", "") cla_logo_url = os.environ.get("CLA_BUCKET_LOGO_URL", "") @@ -92,7 +97,7 @@ class GitHubUserIndex(GlobalSecondaryIndex): class Meta: """Meta class for GitHub User index.""" - index_name = "github-user-index" + index_name = "github-id-index" write_capacity_units = int(cla.conf["DYNAMO_WRITE_UNITS"]) read_capacity_units = int(cla.conf["DYNAMO_READ_UNITS"]) # All attributes are projected - not sure if this is necessary. @@ -136,6 +141,22 @@ class Meta: signature_company_signatory_id = UnicodeAttribute(hash_key=True) +class SignatureProjectReferenceIndex(GlobalSecondaryIndex): + """ + This class represents a global secondary index for querying signatures by project reference ID + """ + + class Meta: + """ Meta class for Signature Project Reference Index """ + + index_name = "signature-project-reference-index" + write_capacity_units = 10 + read_capacity_units = 10 + projection = AllProjection() + + signature_project_id = UnicodeAttribute(hash_key=True) + signature_reference_id = UnicodeAttribute(range_key=True) + class SignatureCompanyInitialManagerIndex(GlobalSecondaryIndex): """ This class represents a global secondary index for querying signatures by signature company initial manager ID @@ -167,6 +188,36 @@ class Meta: user_github_username = UnicodeAttribute(hash_key=True) +class GitLabIDIndex(GlobalSecondaryIndex): + """ + This class represents a global secondary index for querying users by github username. + """ + + class Meta: + index_name = "gitlab-id-index" + write_capacity_units = int(cla.conf["DYNAMO_WRITE_UNITS"]) + read_capacity_units = int(cla.conf["DYNAMO_READ_UNITS"]) + projection = AllProjection() + + # This attribute is the hash key for the index. + user_gitlab_id = UnicodeAttribute(hash_key=True) + + +class GitLabUsernameIndex(GlobalSecondaryIndex): + """ + This class represents a global secondary index for querying users by github username. + """ + + class Meta: + index_name = "gitlab-username-index" + write_capacity_units = int(cla.conf["DYNAMO_WRITE_UNITS"]) + read_capacity_units = int(cla.conf["DYNAMO_READ_UNITS"]) + projection = AllProjection() + + # This attribute is the hash key for the index. + user_gitlab_username = UnicodeAttribute(hash_key=True) + + class LFUsernameIndex(GlobalSecondaryIndex): """ This class represents a global secondary index for querying users by LF Username. @@ -400,6 +451,86 @@ class Meta: organization_sfid = UnicodeAttribute(hash_key=True) +class GitlabOrgSFIndex(GlobalSecondaryIndex): + """ + This class represents a global secondary index for querying gitlab organizations by a Salesforce ID. + """ + + class Meta: + """Meta class for external ID github org index.""" + + index_name = "gitlab-org-sfid-index" + write_capacity_units = int(cla.conf["DYNAMO_WRITE_UNITS"]) + read_capacity_units = int(cla.conf["DYNAMO_READ_UNITS"]) + projection = AllProjection() + + organization_sfid = UnicodeAttribute(hash_key=True) + + +class GitlabOrgProjectSfidOrganizationNameIndex(GlobalSecondaryIndex): + """ + This class represents a global secondary index for querying gitlab organizations by a Project sfid and + Organization Name. + """ + + class Meta: + """Meta class for external ID github org index.""" + + index_name = "gitlab-project-sfid-organization-name-index" + write_capacity_units = int(cla.conf["DYNAMO_WRITE_UNITS"]) + read_capacity_units = int(cla.conf["DYNAMO_READ_UNITS"]) + projection = AllProjection() + + project_sfid = UnicodeAttribute(hash_key=True) + organization_name = UnicodeAttribute(range_key=True) + + +class GitlabOrganizationNameLowerIndex(GlobalSecondaryIndex): + """ + This class represents a global secondary index for querying gitlab organizations by Organization Name. + """ + + class Meta: + """Meta class for external ID github org index.""" + + index_name = "gitlab-organization-name-lower-search-index" + write_capacity_units = int(cla.conf["DYNAMO_WRITE_UNITS"]) + read_capacity_units = int(cla.conf["DYNAMO_READ_UNITS"]) + projection = AllProjection() + + organization_name_lower = UnicodeAttribute(hash_key=True) + +class OrganizationNameLowerSearchIndex(GlobalSecondaryIndex): + """ + This class represents a global secondary index for querying organizations by Organization Name. + """ + + class Meta: + """Meta class for external ID github org index.""" + + index_name = "organization-name-lower-search-index" + write_capacity_units = int(cla.conf["DYNAMO_WRITE_UNITS"]) + read_capacity_units = int(cla.conf["DYNAMO_READ_UNITS"]) + projection = AllProjection() + + organization_name_lower = UnicodeAttribute(hash_key=True) + +class GitlabExternalGroupIDIndex(GlobalSecondaryIndex): + """ + This class represents a global secondary index for querying gitlab organizations by group ID + """ + + class Meta: + """Meta class for external ID for gitlab group id index""" + + index_name = "gitlab-external-group-id-index" + write_capacity_units = int(cla.conf["DYNAMO_WRITE_UNITS"]) + read_capacity_units = int(cla.conf["DYNAMO_READ_UNITS"]) + projection = AllProjection() + + external_gitlab_group_id = NumberAttribute(hash_key=True) + + class GerritProjectIDIndex(GlobalSecondaryIndex): """ This class represents a global secondary index for querying gerrit's by the project ID @@ -583,8 +714,8 @@ class BaseModel(Model): Base pynamodb model used for all CLA models. """ - date_created = UTCDateTimeAttribute(default=datetime.datetime.now()) - date_modified = UTCDateTimeAttribute(default=datetime.datetime.now()) + date_created = UTCDateTimeAttribute(default=datetime.datetime.utcnow()) + date_modified = UTCDateTimeAttribute(default=datetime.datetime.utcnow()) version = UnicodeAttribute(default="v1") # Schema version. def __iter__(self): @@ -1020,6 +1151,7 @@ class Meta: project_live = BooleanAttribute(default=False) foundation_sfid = UnicodeAttribute(null=True) root_project_repositories_count = NumberAttribute(null=True) + note = UnicodeAttribute(null=True) # Indexes project_external_id_index = ExternalProjectIndex() project_name_search_index = ProjectNameIndex() @@ -1047,6 +1179,7 @@ def __init__( project_ccla_requires_icla_signature=False, project_acl=None, project_live=False, + note=None ): super(Project).__init__() self.model = ProjectModel() @@ -1059,6 +1192,7 @@ def __init__( self.model.project_ccla_requires_icla_signature = project_ccla_requires_icla_signature self.model.project_acl = project_acl self.model.project_live = project_live + self.model.note = note def __str__(self): return ( @@ -1103,7 +1237,7 @@ def to_dict(self): return project_dict - def save(self): + def save(self) -> None: self.model.date_modified = datetime.datetime.utcnow() self.model.save() @@ -1267,6 +1401,9 @@ def get_date_created(self): def get_date_modified(self): return self.model.date_modified + def get_note(self) -> Optional[str]: + return self.model.note + def set_project_id(self, project_id): self.model.project_id = str(project_id) @@ -1294,6 +1431,9 @@ def set_project_ccla_enabled(self, project_ccla_enabled): def set_project_live(self, project_live): self.model.project_live = project_live + def set_note(self, note: str) -> None: + self.model.note = note + def add_project_individual_document(self, document): self.model.project_individual_documents.append(document.model) @@ -1433,6 +1573,10 @@ class Meta: user_github_id = NumberAttribute(null=True) user_github_username = UnicodeAttribute(null=True) user_github_username_index = GitHubUsernameIndex() + user_gitlab_id = NumberAttribute(null=True) + user_gitlab_username = UnicodeAttribute(null=True) + user_gitlab_id_index = GitLabIDIndex() + user_gitlab_username_index = GitLabUsernameIndex() user_ldap_id = UnicodeAttribute(null=True) user_github_id_index = GitHubUserIndex() github_user_external_id_index = GithubUserExternalIndex() @@ -1454,6 +1598,8 @@ def __init__( user_external_id=None, user_github_id=None, user_github_username=None, + user_gitlab_id=None, + user_gitlab_username=None, user_ldap_id=None, lf_username=None, lf_sub=None, @@ -1477,12 +1623,14 @@ def __init__( self.model.user_company_id = user_company_id self.model.note = note self._preferred_email = preferred_email + self.model.user_gitlab_id = user_gitlab_id + self.model.user_gitlab_username = user_gitlab_username def __str__(self): return ( "id: {}, username: {}, gh id: {}, gh username: {}, " "lf email: {}, emails: {}, ldap id: {}, lf username: {}, " - "user company id: {}, note: {}, user external id: {}" + "user company id: {}, note: {}, user external id: {}, user gitlab id: {}, user gitlab username: {}" ).format( self.model.user_id, self.model.user_github_username, @@ -1494,7 +1642,9 @@ def __str__(self): self.model.lf_username, self.model.user_company_id, self.model.note, - self.model.user_external_id + self.model.user_external_id, + self.model.user_gitlab_id, + self.model.user_gitlab_username, ) def to_dict(self): @@ -1503,6 +1653,8 @@ def to_dict(self): ret["user_github_id"] = None if ret["user_ldap_id"] == "null": ret["user_ldap_id"] = None + if ret["user_gitlab_id"] == "null": + ret["user_gitlab_id"] = None return ret def log_info(self, msg): @@ -1529,7 +1681,7 @@ def log_warning(self, msg): """ cla.log.warning("{} for user: {}".format(msg, self)) - def save(self): + def save(self) -> None: self.model.date_modified = datetime.datetime.utcnow() self.model.save() @@ -1600,6 +1752,12 @@ def get_user_github_id(self): def get_github_username(self): return self.model.user_github_username + def get_user_gitlab_id(self): + return self.model.user_gitlab_id + + def get_user_gitlab_username(self): + return self.model.user_gitlab_username + def get_user_github_username(self): """ Getter for the user's GitHub ID. @@ -1655,6 +1813,12 @@ def set_user_github_id(self, user_github_id): def set_user_github_username(self, user_github_username): self.model.user_github_username = user_github_username + def set_user_gitlab_id(self, user_gitlab_id): + self.model.user_gitlab_id = user_gitlab_id + + def set_user_gitlab_username(self, user_gitlab_username): + self.model.user_gitlab_username = user_gitlab_username + def set_note(self, note): self.model.note = note @@ -1673,13 +1837,13 @@ def get_user_by_email(self, user_email) -> Optional[List[User]]: else: return None - def get_user_by_github_id(self, user_github_id) -> Optional[List[User]]: + def get_user_by_github_id(self, user_github_id: int) -> Optional[List[User]]: if user_github_id is None: cla.log.warning("Unable to lookup user by github id - id is empty") return None users = [] - for user_model in self.model.user_github_id_index.query(user_github_id): + for user_model in self.model.user_github_id_index.query(int(user_github_id)): user = User() user.model = user_model users.append(user) @@ -1943,6 +2107,62 @@ def is_approved(self, ccla_signature: Signature) -> bool: else: cla.log.debug(f'{fn} - user\'s github_username is not defined - skipping github org approval list check') + # Check GitLab username and id + gitlab_username = self.get_user_gitlab_username() + gitlab_id = self.get_user_gitlab_id() + + # Attempt to fetch the gitlab username based on the gitlab id + if gitlab_username is None and gitlab_id is not None: + github_username = cla.utils.lookup_user_gitlab_username(gitlab_id) + if gitlab_username is not None: + cla.log.debug(f'{fn} - updating user record - adding gitlab username: {gitlab_username}') + self.set_user_gitlab_username(gitlab_username) + self.save() + + # Attempt to fetch the gitlab id based on the gitlab username + if gitlab_id is None and gitlab_username is not None: + gitlab_username = gitlab_username.strip() + gitlab_id = cla.utils.lookup_user_gitlab_id(gitlab_username) + if gitlab_id is not None: + cla.log.debug(f'{fn} - updating user record - adding gitlab id: {gitlab_id}') + self.set_user_gitlab_id(gitlab_id) + self.save() + + # GitLab username approval list processing + if gitlab_username is not None: + # remove leading and trailing whitespace from gitlab username + gitlab_username = gitlab_username.strip() + gitlab_whitelist = ccla_signature.get_gitlab_username_approval_list() + cla.log.debug(f'{fn} - testing user github username: {gitlab_username} with ' + f'CCLA github approval list: {gitlab_whitelist}') + + if gitlab_whitelist is not None: + # case insensitive search + if gitlab_username.lower() in (s.lower() for s in gitlab_whitelist): + cla.log.debug(f'{fn} - found gitlab username in gitlab approval list') + return True + else: + cla.log.debug(f'{fn} - users gitlab_username is not defined - ' + 'skipping gitlab username approval list check') + + if gitlab_username is not None: + cla.log.debug(f'{fn} fetching gitlab org approval list items to search by username: {gitlab_username}') + gitlab_org_approval_lists = ccla_signature.get_gitlab_org_approval_list() + cla.log.debug(f'{fn} checking gitlab org approval list: {gitlab_org_approval_lists}') + if gitlab_org_approval_lists: + for gl_name in gitlab_org_approval_lists: + try: + gl_org = GitlabOrg().search_organization_by_group_url(gl_name) + cla.log.debug( + f"{fn} checking gitlab_username against approval list for gitlab group: {gl_name}") + gl_list = list(filter(lambda gl_user: gl_user.get('username') == gitlab_username, + cla.utils.lookup_gitlab_org_members(gl_org.get_organization_id()))) + if len(gl_list) > 0: + cla.log.debug(f'{fn} - found gitlab username in gitlab approval list') + return True + except DoesNotExist as err: + cla.log.debug(f'gitlab group with full path: {gl_name} does not exist: {err}') + cla.log.debug(f'{fn} - unable to find user in any whitelist') return False @@ -1987,14 +2207,14 @@ class Meta: repository_url = UnicodeAttribute() repository_organization_name = UnicodeAttribute() repository_external_id = UnicodeAttribute(null=True) - repository_project_index = ProjectRepositoryIndex() - project_sfid_repository_index = ProjectSFIDRepositoryIndex() repository_sfdc_id = UnicodeAttribute(null=True) project_sfid = UnicodeAttribute(null=True) - repository_external_index = ExternalRepositoryIndex() - repository_sfdc_index = SFDCRepositoryIndex() enabled = BooleanAttribute(default=False) note = UnicodeAttribute(null=True) + repository_external_index = ExternalRepositoryIndex() + repository_project_index = ProjectRepositoryIndex() + project_sfid_repository_index = ProjectSFIDRepositoryIndex() + repository_sfdc_index = SFDCRepositoryIndex() class Repository(model_interfaces.Repository): @@ -2030,7 +2250,7 @@ def __init__( def to_dict(self): return dict(self.model) - def save(self): + def save(self) -> None: self.model.date_modified = datetime.datetime.utcnow() self.model.save() @@ -2056,7 +2276,25 @@ def get_repository_by_project_sfid(self, project_sfid) -> List[dict]: for repository_model in repository_generator: repository = Repository() repository.model = repository_model - repositories.append(repository.to_dict()) + repositories.append(repository) + return repositories + + def get_repository_models_by_repository_sfdc_id(self, project_sfid) -> List[Repository]: + repository_generator = self.model.repository_sfdc_index.query(project_sfid) + repositories = [] + for repository_model in repository_generator: + repository = Repository() + repository.model = repository_model + repositories.append(repository) + return repositories + + def get_repository_models_by_repository_cla_group_id(self, cla_group_id: str) -> List[Repository]: + repository_generator = self.model.repository_project_index.query(cla_group_id) + repositories = [] + for repository_model in repository_generator: + repository = Repository() + repository.model = repository_model + repositories.append(repository) return repositories def delete(self): @@ -2224,7 +2462,7 @@ class Meta: signature_project_id = UnicodeAttribute() signature_document_minor_version = NumberAttribute() signature_document_major_version = NumberAttribute() - signature_reference_id = UnicodeAttribute() + signature_reference_id = UnicodeAttribute(range_key=True) signature_reference_name = UnicodeAttribute(null=True) signature_reference_name_lower = UnicodeAttribute(null=True) signature_reference_type = UnicodeAttribute() @@ -2242,7 +2480,7 @@ class Meta: signature_return_url = UnicodeAttribute(null=True) signature_callback_url = UnicodeAttribute(null=True) signature_user_ccla_company_id = UnicodeAttribute(null=True) - signature_acl = UnicodeSetAttribute(default=set()) + signature_acl = UnicodeSetAttribute() signature_project_index = ProjectSignatureIndex() signature_reference_index = ReferenceSignatureIndex() signature_envelope_id = UnicodeAttribute(null=True) @@ -2260,12 +2498,15 @@ class Meta: signature_company_signatory_index = SignatureCompanySignatoryIndex() signature_company_initial_manager_index = SignatureCompanyInitialManagerIndex() project_signature_external_id_index = SignatureProjectExternalIndex() + signature_project_reference_index = SignatureProjectReferenceIndex() # approval lists (previously called whitelists) are only used by CCLAs domain_whitelist = ListAttribute(null=True) email_whitelist = ListAttribute(null=True) github_whitelist = ListAttribute(null=True) github_org_whitelist = ListAttribute(null=True) + gitlab_org_approval_list = ListAttribute(null=True) + gitlab_username_approval_list = ListAttribute(null=True) # Additional attributes for ICLAs user_email = UnicodeAttribute(null=True) @@ -2276,6 +2517,8 @@ class Meta: user_docusign_date_signed = UnicodeAttribute(null=True) user_docusign_raw_xml = UnicodeAttribute(null=True) + auto_create_ecla = BooleanAttribute(default=False) + class Signature(model_interfaces.Signature): # pylint: disable=too-many-public-methods """ @@ -2324,8 +2567,14 @@ def __init__( user_name=None, user_docusign_name=None, user_docusign_date_signed=None, + auto_create_ecla: bool = False, ): super(Signature).__init__() + + # Patch the deserialize function of the ListAttribute - this addresses the issue when the List is 'None' + # See notes below in the patched function which describes the problem in more details + attributes.ListAttribute.deserialize = patched_deserialize + self.model = SignatureModel() self.model.signature_id = signature_id self.model.signature_external_id = signature_external_id @@ -2369,6 +2618,7 @@ def __init__( self.model.user_docusign_name = user_docusign_name # in format of 2020-12-21T08:29:20.51 self.model.user_docusign_date_signed = user_docusign_date_signed + self.model.auto_create_ecla = auto_create_ecla def __str__(self): return ( @@ -2383,7 +2633,9 @@ def __str__(self): "signature company initial manager id: {}, signature company initial manager name: {}," "signature company initial manager email: {}, signature company secondary manager list: {}," "user_email: {}, user_github_username: {}, user_name: {}, " - "user_docusign_name: {}, user_docusign_date_signed: {}" + "user_docusign_name: {}, user_docusign_date_signed: {}, " + "auto_create_ecla: {}, " + "created_on: {}, updated_on: {}" ).format( self.model.signature_id, self.model.signature_project_id, @@ -2415,7 +2667,10 @@ def __str__(self): self.model.user_github_username, self.model.user_name, self.model.user_docusign_name, - self.model.user_docusign_date_signed + self.model.user_docusign_date_signed, + self.model.auto_create_ecla, + self.model.get_date_created(), + self.model.get_date_modified(), ) def to_dict(self): @@ -2433,8 +2688,9 @@ def to_dict(self): del d[k] return d - def save(self): - self.model.date_modified = datetime.datetime.utcnow() + def save(self) -> None: + self.model.date_modified = datetime.datetime.now(timezone.utc) + cla.log.info(f'saving datetime: {self.model.date_modified}') self.model.save() def load(self, signature_id): @@ -2508,7 +2764,7 @@ def get_signature_user_ccla_company_id(self): return self.model.signature_user_ccla_company_id def get_signature_acl(self): - return self.model.signature_acl + return self.model.signature_acl or set() def get_signature_return_url_type(self): # Refers to either Gerrit or GitHub @@ -2529,6 +2785,12 @@ def get_github_whitelist(self): def get_github_org_whitelist(self): return self.model.github_org_whitelist + def get_gitlab_org_approval_list(self): + return self.model.gitlab_org_approval_list + + def get_gitlab_username_approval_list(self): + return self.model.gitlab_username_approval_list + def get_note(self): return self.model.note @@ -2577,142 +2839,158 @@ def get_user_docusign_date_signed(self): def get_user_docusign_raw_xml(self): return self.model.user_docusign_raw_xml - def set_signature_id(self, signature_id): + def get_auto_create_ecla(self) -> bool: + return self.model.auto_create_ecla + + def set_signature_id(self, signature_id) -> None: self.model.signature_id = str(signature_id) - def set_signature_external_id(self, signature_external_id): + def set_signature_external_id(self, signature_external_id) -> None: self.model.signature_external_id = str(signature_external_id) - def set_signature_project_id(self, project_id): + def set_signature_project_id(self, project_id) -> None: self.model.signature_project_id = str(project_id) - def set_signature_document_minor_version(self, document_minor_version): + def set_signature_document_minor_version(self, document_minor_version) -> None: self.model.signature_document_minor_version = int(document_minor_version) - def set_signature_document_major_version(self, document_major_version): + def set_signature_document_major_version(self, document_major_version) -> None: self.model.signature_document_major_version = int(document_major_version) - def set_signature_type(self, signature_type): + def set_signature_type(self, signature_type) -> None: self.model.signature_type = signature_type - def set_signature_signed(self, signed): + def set_signature_signed(self, signed) -> None: self.model.signature_signed = bool(signed) - def set_signed_on(self, signed_on): + def set_signed_on(self, signed_on) -> None: self.model.signed_on = signed_on - def set_signatory_name(self, signatory_name): + def set_signatory_name(self, signatory_name) -> None: self.model.signatory_name = signatory_name - def set_signing_entity_name(self, signing_entity_name): + def set_signing_entity_name(self, signing_entity_name) -> None: self.model.signing_entity_name = signing_entity_name - def set_sigtype_signed_approved_id(self, sigtype_signed_approved_id): + def set_sigtype_signed_approved_id(self, sigtype_signed_approved_id) -> None: self.model.sigtype_signed_approved_id = sigtype_signed_approved_id - def set_signature_approved(self, approved): + def set_signature_approved(self, approved) -> None: self.model.signature_approved = bool(approved) - def set_signature_sign_url(self, sign_url): + def set_signature_sign_url(self, sign_url) -> None: self.model.signature_sign_url = sign_url - def set_signature_return_url(self, return_url): + def set_signature_return_url(self, return_url) -> None: self.model.signature_return_url = return_url - def set_signature_callback_url(self, callback_url): + def set_signature_callback_url(self, callback_url) -> None: self.model.signature_callback_url = callback_url - def set_signature_reference_id(self, reference_id): + def set_signature_reference_id(self, reference_id) -> None: self.model.signature_reference_id = reference_id - def set_signature_reference_name(self, reference_name): + def set_signature_reference_name(self, reference_name) -> None: self.model.signature_reference_name = reference_name self.model.signature_reference_name_lower = reference_name.lower() - def set_signature_reference_type(self, reference_type): + def set_signature_reference_type(self, reference_type) -> None: self.model.signature_reference_type = reference_type - def set_signature_user_ccla_company_id(self, company_id): + def set_signature_user_ccla_company_id(self, company_id) -> None: self.model.signature_user_ccla_company_id = company_id - def set_signature_acl(self, signature_acl_username): + def set_signature_acl(self, signature_acl_username) -> None: self.model.signature_acl = set([signature_acl_username]) - def set_signature_return_url_type(self, signature_return_url_type): + def set_signature_return_url_type(self, signature_return_url_type) -> None: self.model.signature_return_url_type = signature_return_url_type - def set_signature_envelope_id(self, signature_envelope_id): + def set_signature_envelope_id(self, signature_envelope_id) -> None: self.model.signature_envelope_id = signature_envelope_id - def set_signature_company_signatory_id(self, signature_company_signatory_id): + def set_signature_company_signatory_id(self, signature_company_signatory_id) -> None: self.model.signature_company_signatory_id = signature_company_signatory_id - def set_signature_company_signatory_name(self, signature_company_signatory_name): + def set_signature_company_signatory_name(self, signature_company_signatory_name) -> None: self.model.signature_company_signatory_name = signature_company_signatory_name - def set_signature_company_signatory_email(self, signature_company_signatory_email): + def set_signature_company_signatory_email(self, signature_company_signatory_email) -> None: self.model.signature_company_signatory_email = signature_company_signatory_email - def set_signature_company_initial_manager_id(self, signature_company_initial_manager_id): + def set_signature_company_initial_manager_id(self, signature_company_initial_manager_id) -> None: self.model.signature_company_initial_manager_id = signature_company_initial_manager_id - def set_signature_company_initial_manager_name(self, signature_company_initial_manager_name): + def set_signature_company_initial_manager_name(self, signature_company_initial_manager_name) -> None: self.model.signature_company_initial_manager_name = signature_company_initial_manager_name - def set_signature_company_initial_manager_email(self, signature_company_initial_manager_email): + def set_signature_company_initial_manager_email(self, signature_company_initial_manager_email) -> None: self.model.signature_company_initial_manager_email = signature_company_initial_manager_email - def set_signature_company_secondary_manager_list(self, signature_company_secondary_manager_list): + def set_signature_company_secondary_manager_list(self, signature_company_secondary_manager_list) -> None: self.model.signature_company_secondary_manager_list = signature_company_secondary_manager_list # Remove leading and trailing whitespace for all items before setting whitelist - def set_domain_whitelist(self, domain_whitelist): + def set_domain_whitelist(self, domain_whitelist) -> None: self.model.domain_whitelist = [domain.strip() for domain in domain_whitelist] - def set_email_whitelist(self, email_whitelist): + def set_email_whitelist(self, email_whitelist) -> None: self.model.email_whitelist = [email.strip() for email in email_whitelist] - def set_github_whitelist(self, github_whitelist): + def set_github_whitelist(self, github_whitelist) -> None: self.model.github_whitelist = [github_user.strip() for github_user in github_whitelist] - def set_github_org_whitelist(self, github_org_whitelist): + def set_github_org_whitelist(self, github_org_whitelist) -> None: self.model.github_org_whitelist = [github_org.strip() for github_org in github_org_whitelist] - def set_note(self, note): + def set_gitlab_username_approval_list(self, gitlab_username_approval_list) -> None: + self.model.gitlab_username_approval_list = [gitlab_user.strip() for gitlab_user in + gitlab_username_approval_list] + + def set_gitlab_org_approval_list(self, gitlab_org_approval_list) -> None: + self.model.gitlab_org_approval_list = [gitlab_org.strip() for gitlab_org in gitlab_org_approval_list] + + def set_note(self, note) -> None: self.model.note = note - def set_signature_project_external_id(self, signature_project_external_id): + def set_signature_project_external_id(self, signature_project_external_id) -> None: self.model.signature_project_external_id = signature_project_external_id - def add_signature_acl(self, username): + def add_signature_acl(self, username) -> None: + if not self.model.signature_acl: + self.model.signature_acl = set() self.model.signature_acl.add(username) - def remove_signature_acl(self, username): - if username in self.model.signature_acl: - self.model.signature_acl.remove(username) + def remove_signature_acl(self, username) -> None: + current_acl = self.model.signature_acl or set() + if username not in current_acl: + return + self.model.signature_acl.remove(username) - def set_user_email(self, user_email): + def set_user_email(self, user_email) -> None: self.model.user_email = user_email - def set_user_github_username(self, user_github_username): + def set_user_github_username(self, user_github_username) -> None: self.model.user_github_username = user_github_username - def set_user_name(self, user_name): + def set_user_name(self, user_name) -> None: self.model.user_name = user_name - def set_user_lf_username(self, user_lf_username): + def set_user_lf_username(self, user_lf_username) -> None: self.model.user_lf_username = user_lf_username - def set_user_docusign_name(self, user_docusign_name): + def set_user_docusign_name(self, user_docusign_name) -> None: self.model.user_docusign_name = user_docusign_name - def set_user_docusign_date_signed(self, user_docusign_date_signed): + def set_user_docusign_date_signed(self, user_docusign_date_signed) -> None: self.model.user_docusign_date_signed = user_docusign_date_signed - def set_user_docusign_raw_xml(self, user_docusign_raw_xml): + def set_user_docusign_raw_xml(self, user_docusign_raw_xml) -> None: self.model.user_docusign_raw_xml = user_docusign_raw_xml + def set_auto_create_ecla(self, auto_create_ecla: bool) -> None: + self.model.auto_create_ecla = auto_create_ecla def get_signatures_by_reference( self, # pylint: disable=too-many-arguments reference_id, @@ -2722,78 +3000,68 @@ def get_signatures_by_reference( signature_signed=None, signature_approved=None, ): + fn = 'cla.models.dynamo_models.signature.get_signatures_by_reference' + cla.log.debug(f'{fn} - reference_id: {reference_id}, reference_type: {reference_type},' + f' project_id: {project_id}, user_ccla_company_id: {user_ccla_company_id},' + f' signature_signed: {signature_signed}, signature_approved: {signature_approved}') + + cla.log.debug(f'{fn} - performing signature_reference_id query using: {reference_id}') # TODO: Optimize this query to use filters properly. - # cla.log.debug('Signatures.get_signatures_by_reference() - reference_id: {}, reference_type: {}' - # ' project_id: {}, user_ccla_company_id: {}' - # ' signature_signed: {}, signature_approved: {}'. - # format(reference_id, reference_type, project_id, user_ccla_company_id, signature_signed, - # signature_approved)) - - # cla.log.debug('Signatures.get_signatures_by_reference() - ' - # 'performing signature_reference_id query using: {}'.format(reference_id)) - signature_generator = self.model.signature_reference_index.query(str(reference_id)) - # cla.log.debug('Signatures.get_signatures_by_reference() - generator.last_evaluated_key: {}'. - # format(signature_generator.last_evaluated_key)) + # signature_generator = self.model.signature_reference_index.query(str(reference_id)) + try: + signature_generator = self.model.signature_project_reference_index.query(str(project_id), range_key_condition=SignatureModel.signature_reference_id == str(reference_id)) + except Exception as e: + cla.log.error(f'{fn} - error performing signature_reference_id query using: {reference_id} - ' + f'error: {e}') + raise e signatures = [] for signature_model in signature_generator: + cla.log.debug(f'{fn} - processing signature {signature_model}') + # Skip signatures that are not the same reference type: user/company if signature_model.signature_reference_type != reference_type: - cla.log.debug( - "Signatures.get_signatures_by_reference() - skipping signature - " - "reference types do not match: {} versus {}".format( - signature_model.signature_reference_type, reference_type - ) - ) + cla.log.debug(f"{fn} - skipping signature - " + f"reference types do not match: {signature_model.signature_reference_type} " + f"versus {reference_type}") continue + cla.log.debug(f"{fn} - signature reference types match: {signature_model.signature_reference_type}") # Skip signatures that are not an employee CCLA if user_ccla_company_id is present. # if user_ccla_company_id and signature_user_ccla_company_id are both none # it loads the ICLA signatures for a user. if signature_model.signature_user_ccla_company_id != user_ccla_company_id: - cla.log.debug( - "Signatures.get_signatures_by_reference() - skipping signature - " - "user_ccla_company_id values do not match: {} versus {}".format( - signature_model.signature_user_ccla_company_id, user_ccla_company_id, - ) - ) + cla.log.debug(f"{fn} - skipping signature - " + f"user_ccla_company_id values do not match: " + f"{signature_model.signature_user_ccla_company_id} " + f"versus {user_ccla_company_id}") continue - # Skip signatures that are not of the same project - if project_id is not None and signature_model.signature_project_id != project_id: - cla.log.debug( - "Signatures.get_signatures_by_reference() - skipping signature - " - "project_id values do not match: {} versus {}".format( - signature_model.signature_project_id, project_id - ) - ) - continue + # # Skip signatures that are not of the same project + # if project_id is not None and signature_model.signature_project_id != project_id: + # cla.log.debug(f"{fn} - skipping signature - " + # f"project_id values do not match: {signature_model.signature_project_id} " + # f"versus {project_id}") + # continue - # SKip signatures that do not have the same signed flags + # Skip signatures that do not have the same signed flags # e.g. retrieving only signed / approved signatures if signature_signed is not None and signature_model.signature_signed != signature_signed: - cla.log.debug( - "Signatures.get_signatures_by_reference() - skipping signature - " - "signature_signed values do not match: {} versus {}".format( - signature_model.signature_signed, signature_signed - ) - ) + cla.log.debug(f"{fn} - skipping signature - " + f"signature_signed values do not match: {signature_model.signature_signed} " + f"versus {signature_signed}") continue if signature_approved is not None and signature_model.signature_approved != signature_approved: - cla.log.debug( - "Signatures.get_signatures_by_reference() - skipping signature - " - "signature_approved values do not match: {} versus {}".format( - signature_model.signature_approved, signature_approved - ) - ) + cla.log.debug(f"{fn} - skipping signature - " + f"signature_approved values do not match: {signature_model.signature_approved} " + f"versus {signature_approved}") continue signature = Signature() signature.model = signature_model signatures.append(signature) - # cla.log.debug('Signatures.get_signatures_by_reference() - signature match - ' - # 'adding signature to signature list: {}'.format(signature)) + cla.log.debug(f'{fn} - signature match - adding signature to signature list: {signature}') return signatures def get_signatures_by_project( @@ -2909,6 +3177,35 @@ def get_employee_signature_by_company_project(self, company_id, project_id, user "Why do we have more than one employee signature for this user? - Will return the first one only.") return signatures[0] + def get_employee_signature_by_company_project_list(self, company_id, project_id, user_id) -> Optional[ + List[Signature]]: + """ + Returns the employee signature for the specified user associated with + the project/company. Returns None if no employee signature exists for + this set of query parameters. + """ + signature_attributes = { + "signature_signed": True, + "signature_approved": True, + "signature_type": 'cla', + "signature_reference_type": 'user', + "signature_project_id": project_id, + "signature_user_ccla_company_id": company_id + } + filter_condition = create_filter(signature_attributes, SignatureModel) + signature_generator = self.model.signature_reference_index.query( + user_id, filter_condition=filter_condition + ) + signatures = [] + for signature_model in signature_generator: + signature = Signature() + signature.model = signature_model + signatures.append(signature) + # No employee signatures were found that were signed/approved + if len(signatures) == 0: + return None + return signatures + def get_employee_signatures_by_company_project_model(self, company_id, project_id) -> List[Signature]: signature_attributes = { "signature_signed": True, @@ -2960,18 +3257,28 @@ def get_managers_by_signature_acl(self, signature_acl): def get_managers(self): return self.get_managers_by_signature_acl(self.get_signature_acl()) - def all(self, ids=None): + def all(self, ids: str = None) -> List[Signature]: if ids is None: signatures = self.model.scan() else: signatures = SignatureModel.batch_get(ids) ret = [] for signature in signatures: - agr = Signature() - agr.model = signature - ret.append(agr) + sig = Signature() + sig.model = signature + ret.append(sig) return ret + def all_limit(self, limit: Optional[int] = None, last_evaluated_key: Optional[str] = None) -> \ + (List[Signature], str, int): + result_iterator = self.model.scan(limit=limit, last_evaluated_key=last_evaluated_key) + ret = [] + for signature in result_iterator: + sig = Signature() + sig.model = signature + ret.append(sig) + return ret, result_iterator.last_evaluated_key, result_iterator.total_count + class ProjectCLAGroupModel(BaseModel): """ @@ -3175,6 +3482,7 @@ class Meta: signing_entity_name_index = SigningEntityNameIndex() company_external_id_index = ExternalCompanyIndex() company_acl = UnicodeSetAttribute(default=set()) + note = UnicodeAttribute(null=True) class Company(model_interfaces.Company): # pylint: disable=too-many-public-methods @@ -3190,8 +3498,14 @@ def __init__( company_name=None, signing_entity_name=None, company_acl=None, + note=None, ): super(Company).__init__() + + # Patch the deserialize function of the ListAttribute - this addresses the issue when the List is 'None' + # See notes below in the patched function which describes the problem in more details + attributes.ListAttribute.deserialize = patched_deserialize + self.model = CompanyModel() self.model.company_id = company_id self.model.company_external_id = company_external_id @@ -3202,6 +3516,7 @@ def __init__( else: self.model.signing_entity_name = company_name self.model.company_acl = company_acl + self.model.note = note def __str__(self) -> str: return ( @@ -3210,7 +3525,8 @@ def __str__(self) -> str: f"signing_entity_name: {self.model.signing_entity_name}, " f"external id: {self.model.company_external_id}, " f"manager id: {self.model.company_manager_id}, " - f"acl: {self.model.company_acl}" + f"acl: {self.model.company_acl}, " + f"note: {self.model.note}" ) def to_dict(self) -> dict: @@ -3254,13 +3570,16 @@ def get_company_name(self) -> str: return self.model.company_name def get_signing_entity_name(self) -> str: - if self.model.signing_entity_name is None: - return self.model.company_name + # if self.model.signing_entity_name is None: + # return self.model.company_name return self.model.signing_entity_name def get_company_acl(self) -> Optional[List[str]]: return self.model.company_acl + def get_note(self) -> str: + return self.model.note + def set_company_id(self, company_id: str) -> None: self.model.company_id = company_id @@ -3279,6 +3598,15 @@ def set_signing_entity_name(self, signing_entity_name: str) -> None: def set_company_acl(self, company_acl_username: str) -> None: self.model.company_acl = set([company_acl_username]) + def set_note(self, note: str) -> None: + self.model.note = note + + def update_note(self, note: str) -> None: + if self.model.note: + self.model.note = self.model.note + ' ' + note + else: + self.model.note = note + def set_date_modified(self) -> None: """ Updates the company modified date/time to the current time. @@ -3319,9 +3647,13 @@ def get_latest_signature(self, project_id: str, signature_signed: bool = None, :return: The latest versioned signature object if it exists. :rtype: cla.models.model_interfaces.Signature or None """ + cla.log.debug(f"locating latest signature - project_id={project_id}, " + f"signature_signed={signature_signed}, " + f"signature_approved={signature_approved}") signatures = self.get_company_signatures( project_id=project_id, signature_signed=signature_signed, signature_approved=signature_approved) latest = None + cla.log.debug(f"retrieved {len(signatures)}") for signature in signatures: if latest is None: latest = signature @@ -3346,11 +3678,12 @@ def get_company_by_id(self, company_id: str): def get_company_by_external_id(self, company_external_id: str): company_generator = self.model.company_external_id_index.query(company_external_id) + companies = [] for company_model in company_generator: company = Company() company.model = company_model - return company - return None + companies.append(company) + return companies def all(self, ids: List[str] = None): if ids is None: @@ -3421,9 +3754,13 @@ def set(self, key, value): model.save() def get(self, key): + import json model = StoreModel() try: - return model.get(key).value + val = model.get(key).value + if isinstance(val, dict): + val = json.dumps(val) + return val except StoreModel.DoesNotExist: raise cla.models.DoesNotExist("Key not found") @@ -3441,15 +3778,43 @@ def exists(self, key): return False def get_expire_timestamp(self): - # helper function to set store item ttl: 1 day - exp_datetime = datetime.datetime.now() + datetime.timedelta(days=1) + # helper function to set store item ttl: 7 days + exp_datetime = datetime.datetime.now() + datetime.timedelta(days=7) return exp_datetime.timestamp() +class GitlabOrgModel(BaseModel): + """ + Represents a Gitlab Organization in the database. + """ + + class Meta: + table_name = "cla-{}-gitlab-orgs".format(stage) + if stage == "local": + host = "http://localhost:8000" + + organization_id = UnicodeAttribute(hash_key=True) + organization_name = UnicodeAttribute(null=True) + organization_url = UnicodeAttribute(null=True) + organization_name_lower = UnicodeAttribute(null=True) + organization_sfid = UnicodeAttribute() + external_gitlab_group_id = NumberAttribute() + project_sfid = UnicodeAttribute() + auth_info = UnicodeAttribute() + organization_sfid_index = GitlabOrgSFIndex() + project_sfid_organization_name_index = GitlabOrgProjectSfidOrganizationNameIndex() + organization_name_lower_index = GitlabOrganizationNameLowerIndex() + gitlab_external_group_id_index = GitlabExternalGroupIDIndex() + auto_enabled = BooleanAttribute(null=True) + auto_enabled_cla_group_id = UnicodeAttribute(null=True) + branch_protection_enabled = BooleanAttribute(null=True) + enabled = BooleanAttribute(null=True) + note = UnicodeAttribute(null=True) + + class GitHubOrgModel(BaseModel): """ - Represents a Github Organization in the database. - Company_id, project_id are deprecated now that organizations are under an SFDC ID. + Represents a Gitlab Organization in the database. """ class Meta: @@ -3464,11 +3829,15 @@ class Meta: organization_installation_id = NumberAttribute(null=True) organization_sfid = UnicodeAttribute() project_sfid = UnicodeAttribute() - organization_sfid_index = GithubOrgSFIndex() + organization_sfid_index = GitlabOrgSFIndex() + project_sfid_organization_name_index = GitlabOrgProjectSfidOrganizationNameIndex() + organization_name_lower_index = GitlabOrganizationNameLowerIndex() + organization_name_lower_search_index = OrganizationNameLowerSearchIndex() organization_project_id = UnicodeAttribute(null=True) organization_company_id = UnicodeAttribute(null=True) auto_enabled = BooleanAttribute(null=True) branch_protection_enabled = BooleanAttribute(null=True) + enabled = BooleanAttribute(null=True) note = UnicodeAttribute(null=True) @@ -3479,7 +3848,7 @@ class GitHubOrg(model_interfaces.GitHubOrg): # pylint: disable=too-many-public- def __init__( self, organization_name=None, organization_installation_id=None, organization_sfid=None, - auto_enabled=False, branch_protection_enabled=False, note=None, + auto_enabled=False, branch_protection_enabled=False, note=None, enabled=True ): super(GitHubOrg).__init__() self.model = GitHubOrgModel() @@ -3491,6 +3860,7 @@ def __init__( self.model.auto_enabled = auto_enabled self.model.branch_protection_enabled = branch_protection_enabled self.model.note = note + self.model.enabled = enabled def __str__(self): return ( @@ -3502,6 +3872,7 @@ def __str__(self): f'auto_enabled: {self.model.auto_enabled},' f'branch_protection_enabled: {self.model.branch_protection_enabled},' f'note: {self.model.note}' + f'enabled: {self.model.enabled}' ) def to_dict(self): @@ -3512,7 +3883,7 @@ def to_dict(self): ret["organization_sfid"] = None return ret - def save(self): + def save(self) -> None: self.model.date_modified = datetime.datetime.utcnow() self.model.save() @@ -3555,6 +3926,9 @@ def get_note(self): """ return self.model.note + def get_enabled(self): + return self.model.enabled + def set_organization_name(self, organization_name): self.model.organization_name = organization_name if self.model.organization_name: @@ -3584,6 +3958,9 @@ def set_branch_protection_enabled(self, branch_protection_enabled): def set_note(self, note): self.model.note = note + def set_enabled(self, enabled): + self.model.enabled = enabled + def get_organization_by_sfid(self, sfid) -> List: organization_generator = self.model.organization_sfid_index.query(sfid) organizations = [] @@ -3602,7 +3979,7 @@ def get_organization_by_installation_id(self, installation_id): return None def get_organization_by_lower_name(self, organization_name): - org_generator = self.model.scan(organization_name_lower__eq=organization_name.lower()) + org_generator = self.model.organization_name_lower_search_index.query(organization_name.lower()) for org_model in org_generator: org = GitHubOrg() org.model = org_model @@ -3619,6 +3996,210 @@ def all(self): return ret +class GitlabOrg(model_interfaces.GitlabOrg): # pylint: disable=too-many-public-methods + """ + ORM-agnostic wrapper for the DynamoDB GitlabOrg model. + """ + + def __init__( + self, organization_id=None, organization_name=None, organization_sfid=None, auth_info=None, + project_sfid=None, auto_enabled=False, branch_protection_enabled=False, note=None, enabled=True + ): + super(GitlabOrg).__init__() + self.model = GitlabOrgModel() + if not organization_id: + organization_id = str(uuid.uuid4()) + self.model.organization_id = organization_id + + self.model.organization_name = organization_name + if self.model.organization_name: + self.model.organization_name_lower = self.model.organization_name.lower() + + self.model.organization_sfid = organization_sfid + self.model.project_sfid = project_sfid + self.model.auto_enabled = auto_enabled + self.model.branch_protection_enabled = branch_protection_enabled + self.model.enabled = enabled + self.model.note = note + self.model.auth_info = auth_info + + def __str__(self): + return ( + f'organization id:{self.model.organization_id}, ' + f'organization name:{self.model.organization_name}, ' + f'organization url : {self.model.organization_url}, ' + f'organization SFID: {self.model.organization_sfid}, ' + f'auto_enabled: {self.model.auto_enabled},' + f'branch_protection_enabled: {self.model.branch_protection_enabled},' + f'enabled: {self.model.enabled},' + f'note: {self.model.note}', + f'auth_info: {self.model.auth_info}' + f'external_gitlab_group_id: {self.model.external_gitlab_group_id}' + ) + + def to_dict(self): + ret = dict(self.model) + if ret["organization_sfid"] == "null": + ret["organization_sfid"] = None + return ret + + def save(self) -> None: + self.model.date_modified = datetime.datetime.utcnow() + self.model.save() + + def load(self, organization_id: str): + try: + organization = self.model.get(organization_id) + except GitlabOrgModel.DoesNotExist: + raise cla.models.DoesNotExist("Gitlab Org not found") + self.model = organization + + def delete(self): + self.model.delete() + + def get_external_gitlab_group_id(self): + return self.model.external_gitlab_group_id + + def get_organization_id(self): + return self.model.organization_id + + def get_organization_url(self): + return self.model.organization_url + + def get_organization_name(self): + return self.model.organization_name + + def get_organization_sfid(self): + return self.model.organization_sfid + + def get_project_sfid(self): + return self.model.project_sfid + + def get_organization_name_lower(self): + return self.model.organization_name_lower + + def get_auto_enabled(self): + return self.model.auto_enabled + + def get_branch_protection_enabled(self): + return self.model.branch_protection_enabled + + def get_note(self): + """ + Getter for the note. + :return: the note value for the github organization record + :rtype: str + """ + return self.model.note + + def get_auth_info(self): + return self.model.auth_info + + def get_enabled(self): + return self.model.enabled + + def set_external_gitlab_group_id(self, external_gitlab_group_id): + self.model.external_gitlab_group_id = external_gitlab_group_id + + def set_organization_name(self, organization_name): + self.model.organization_name = organization_name + if self.model.organization_name: + self.model.organization_name_lower = self.model.organization_name.lower() + + def set_organization_url(self, organization_url): + self.model.organization_url = organization_url + + def set_organization_sfid(self, organization_sfid): + self.model.organization_sfid = organization_sfid + + def set_project_sfid(self, project_sfid): + self.model.project_sfid = project_sfid + + def set_organization_name_lower(self, organization_name_lower): + self.model.organization_name_lower = organization_name_lower + + def set_auto_enabled(self, auto_enabled): + self.model.auto_enabled = auto_enabled + + def set_branch_protection_enabled(self, branch_protection_enabled): + self.model.branch_protection_enabled = branch_protection_enabled + + def set_note(self, note): + self.model.note = note + + def set_enabled(self, enabled): + self.model.enabled = enabled + + def set_auth_info(self, auth_info): + self.model.auth_info = auth_info + + def get_organization_by_groupid(self, groupid): + org_generator = self.model.gitlab_external_group_id_index.query(groupid) + for org_model in org_generator: + org = GitlabOrg() + org.model = org_model + return org + return None + + def get_organization_by_sfid(self, sfid) -> List: + organization_generator = self.model.organization_sfid_index.query(sfid) + organizations = [] + for org_model in organization_generator: + org = GitlabOrg() + org.model = org_model + organizations.append(org) + return organizations + + def search_organization_by_lower_name(self, organization_name): + organizations = list( + filter(lambda org: org.get_organization_name_lower() == organization_name.lower(), self.all())) + if organizations: + return organizations[0] + raise cla.models.DoesNotExist(f"Gitlab Org : {organization_name} does not exist") + + def search_organization_by_group_url(self, group_url): + # first check for match.. could be in the format https://gitlab.com/groups/" in email_body + assert "EasyCLA regarding the project(s) project1, project2 associated" in email_body + assert "with the CLA Group cla_group_name_value" in email_body + assert "john has designated you as an authorized signatory" in email_body + assert "signatory for the organization IBM" in email_body + assert "
After you sign, john (as the initial CLA Manager for your company)" in email_body
+ assert "and if you approve john as your initial CLA Manager" in email_body
+ assert "contact the requester at john@example.com" in email_body
diff --git a/cla-backend/cla/tests/unit/test_dynamo_models.py b/cla-backend/cla/tests/unit/test_dynamo_models.py
index 5045650aa..475fffa53 100644
--- a/cla-backend/cla/tests/unit/test_dynamo_models.py
+++ b/cla-backend/cla/tests/unit/test_dynamo_models.py
@@ -4,9 +4,9 @@
from unittest.mock import Mock
import pytest
-
-from cla.models.dynamo_models import User, Company
from cla import utils
+from cla.models.dynamo_models import Company, User
+
@pytest.fixture
def user():
diff --git a/cla-backend/cla/tests/unit/test_ecla.py b/cla-backend/cla/tests/unit/test_ecla.py
new file mode 100644
index 000000000..42be1c905
--- /dev/null
+++ b/cla-backend/cla/tests/unit/test_ecla.py
@@ -0,0 +1,53 @@
+import unittest
+from unittest.mock import Mock, patch
+import datetime
+
+from cla.models.docusign_models import DocuSign
+
+def test_save_employee_signature(project, company, user_instance):
+ """ Test _save_employee_signature """
+ # Mock DocuSign method and related class methods
+ DocuSign.check_and_prepare_employee_signature = Mock(return_value={'success': {'the employee is ready to sign the CCLA'}})
+
+ # Create an instance of DocuSign and mock its dynamo_client
+ docusign = DocuSign()
+ docusign.dynamo_client = Mock() # Mock the dynamo_client on the instance
+ mock_put_item = docusign.dynamo_client.put_item = Mock()
+
+ # Mock ecla signature object with necessary attributes for the helper method
+ signature = Mock()
+ signature.get_signature_id.return_value = "sig_id"
+ signature.get_signature_project_id.return_value = "proj_id"
+ signature.get_signature_document_minor_version.return_value = 1
+ signature.get_signature_document_major_version.return_value = 2
+ signature.get_signature_reference_id.return_value = "ref_id"
+ signature.get_signature_reference_type.return_value = "user"
+ signature.get_signature_type.return_value = "cla"
+ signature.get_signature_signed.return_value = True
+ signature.get_signature_approved.return_value = True
+ signature.get_signature_acl.return_value = ['acl1', 'acl2']
+ signature.get_signature_user_ccla_company_id.return_value = "company_id"
+ signature.get_signature_return_url.return_value = None
+ signature.get_signature_reference_name.return_value = None
+
+ # Call the helper method
+ docusign._save_employee_signature(signature)
+
+ # Check if dynamo_client.put_item was called
+ assert mock_put_item.called
+
+ # Extract the 'Item' argument passed to put_item
+ _, kwargs = mock_put_item.call_args
+ item = kwargs['Item']
+
+ # Assert that 'date_modified' and 'date_created' are in the item
+ assert 'date_modified' in item
+ assert 'date_created' in item
+
+
+ # Optionally, check if they are correctly formatted ISO timestamps
+ try:
+ datetime.datetime.fromisoformat(item['date_modified']['S'])
+ datetime.datetime.fromisoformat(item['date_created']['S'])
+ except ValueError:
+ assert False, "date_modified or date_created are not valid ISO format timestamps"
diff --git a/cla-backend/cla/tests/unit/test_email_approval_list.py b/cla-backend/cla/tests/unit/test_email_approval_list.py
index 79fb94409..a2975689c 100644
--- a/cla-backend/cla/tests/unit/test_email_approval_list.py
+++ b/cla-backend/cla/tests/unit/test_email_approval_list.py
@@ -1,10 +1,9 @@
# Copyright The Linux Foundation and each contributor to CommunityBridge.
# SPDX-License-Identifier: MIT
-from unittest.mock import patch, MagicMock
+from unittest.mock import MagicMock, patch
import pytest
-
from cla.models.dynamo_models import Signature, User, UserModel
diff --git a/cla-backend/cla/tests/unit/test_event.py b/cla-backend/cla/tests/unit/test_event.py
index 9769b05c6..05e572852 100644
--- a/cla-backend/cla/tests/unit/test_event.py
+++ b/cla-backend/cla/tests/unit/test_event.py
@@ -1,13 +1,13 @@
# Copyright The Linux Foundation and each contributor to CommunityBridge.
# SPDX-License-Identifier: MIT
-from cla.models.dynamo_models import Event, User, Project, Company
-from cla.models import event_types
-from unittest.mock import patch, Mock
-import pytest
import datetime
-import cla
import time
+from unittest.mock import Mock
+
+import pytest
+from cla.models import event_types
+from cla.models.dynamo_models import Company, Event, Project, User
@pytest.fixture()
@@ -16,6 +16,7 @@ def mock_event():
event.model.save = Mock()
yield event
+
def test_event_user_id(user_instance):
""" Test event_user_id """
Event.save = Mock()
@@ -29,9 +30,10 @@ def test_event_user_id(user_instance):
)
assert 'data' in response
+
def test_event_company_id(company):
""" Test creation of event instance """
- #Case for creating Company
+ # Case for creating Company
Event.save = Mock()
Company.load = Mock()
event_data = 'test company created'
@@ -43,6 +45,7 @@ def test_event_company_id(company):
)
assert 'data' in response
+
def test_event_project_id(project):
""" Test event with event_project_id """
Event.save = Mock()
@@ -52,9 +55,10 @@ def test_event_project_id(project):
event_data=event_data,
event_summary=event_data,
event_type=event_types.EventType.DeleteProject,
- event_project_id=project.get_project_id()
+ event_cla_group_id=project.get_project_id()
)
- assert 'data' in response
+ assert project.get_project_id() == response['data']['event_cla_group_id']
+
def test_event_user_id_attribute(user_instance, mock_event):
""" Test event_user_id attribute """
@@ -62,56 +66,62 @@ def test_event_user_id_attribute(user_instance, mock_event):
mock_event.save()
assert mock_event.get_event_user_id() == user_instance.get_user_id()
+
def test_event_company_name_lower_attribute(mock_event):
""" Test company_name_lower attribute """
mock_event.set_event_company_name("Company_lower")
mock_event.save()
assert mock_event.get_event_company_name_lower() == "company_lower"
+
def test_event_username_attribute(mock_event):
""" Test event_username attribute """
mock_event.set_event_user_name("foo_username")
mock_event.save()
assert mock_event.get_event_user_name() == "foo_username"
+
def test_event_user_name_lower_attribute(mock_event):
""" Test event_user_name_lower attribute """
mock_event.set_event_user_name("Username")
mock_event.save()
assert mock_event.get_event_user_name_lower() == "username"
+
def test_event_project_name_lower_attribute(mock_event):
- """ Test gettting project """
+ """ Test getting project """
mock_event.set_event_project_name("Project")
mock_event.save()
assert mock_event.get_event_project_name_lower() == "project"
+
def test_event_time(mock_event):
""" Test event time """
mock_event.save()
- assert mock_event.get_event_time() <= datetime.datetime.now()
+ assert mock_event.get_event_time() <= datetime.datetime.utcnow()
+
+
-def test_event_time_epoch(mock_event):
- """ Test event time epoch """
- mock_event.save()
- assert mock_event.get_event_time_epoch() <= time.time()
def test_company_id_external_project_id(mock_event):
- mock_event.set_event_project_external_id("external_id")
+ mock_event.set_event_project_sfid("external_id")
mock_event.set_event_company_id("company_id")
mock_event.set_company_id_external_project_id()
assert mock_event.get_company_id_external_project_id() == "company_id#external_id"
+
def test_company_id_external_project_id_empty_test1(mock_event):
- mock_event.set_event_project_external_id("external_id")
+ mock_event.set_event_project_sfid("external_id")
mock_event.set_company_id_external_project_id()
assert mock_event.get_company_id_external_project_id() == None
+
def test_company_id_external_project_id_empty_test2(mock_event):
mock_event.set_event_company_id("company_id")
mock_event.set_company_id_external_project_id()
assert mock_event.get_company_id_external_project_id() == None
+
def test_company_id_external_project_id_empty_test3(mock_event):
mock_event.set_company_id_external_project_id()
assert mock_event.get_company_id_external_project_id() == None
diff --git a/cla-backend/cla/tests/unit/test_gh_org_models.py b/cla-backend/cla/tests/unit/test_gh_org_models.py
index 1a49e3083..0828d0d7e 100644
--- a/cla-backend/cla/tests/unit/test_gh_org_models.py
+++ b/cla-backend/cla/tests/unit/test_gh_org_models.py
@@ -1,14 +1,14 @@
# Copyright The Linux Foundation and each contributor to CommunityBridge.
# SPDX-License-Identifier: MIT
-import pytest
+from unittest.mock import MagicMock, Mock, patch
+
import cla
import pynamodb
-from unittest.mock import Mock, patch, MagicMock
-
+import pytest
from cla.models.dynamo_models import GitHubOrg, GitHubOrgModel
-from cla.utils import get_github_organization_instance
from cla.tests.unit.data import GH_TABLE_DESCRIPTION
+from cla.utils import get_github_organization_instance
PATCH_METHOD = "pynamodb.connection.Connection._make_api_call"
@@ -35,6 +35,6 @@ def test_set_organization_name(gh_instance):
def test_get_org_by_name_lower(gh_instance):
""" Test getting GitHub org with case insensitive search """
gh_org = cla.utils.get_github_organization_instance()
- gh_org.model.scan = Mock(return_value=[gh_instance.model])
+ gh_org.model.organization_name_lower_search_index.query = Mock(return_value=[gh_instance.model])
found_gh_org = gh_org.get_organization_by_lower_name(gh_instance.get_organization_name())
assert found_gh_org.get_organization_name_lower() == gh_instance.get_organization_name_lower()
diff --git a/cla-backend/cla/tests/unit/test_github.py b/cla-backend/cla/tests/unit/test_github.py
index 983a7d12a..1d4229cd4 100644
--- a/cla-backend/cla/tests/unit/test_github.py
+++ b/cla-backend/cla/tests/unit/test_github.py
@@ -2,9 +2,11 @@
# SPDX-License-Identifier: MIT
import unittest
+from unittest.mock import MagicMock
import cla
-from cla.controllers.github import webhook_secret_validation, webhook_secret_failed_email_content
+from cla.controllers.github import webhook_secret_failed_email_content, webhook_secret_validation
+from cla.models.github_models import has_check_previously_passed_or_failed
from cla.utils import get_comment_badge
SUCCESS = ":white_check_mark:"
@@ -118,6 +120,42 @@ def test_comment_badge_with_missing_whitelisted_user():
assert confirmation_needed_badge in response
+def test_has_check_previously_passed_or_failed_failed_status():
+ # Create a mock PullRequest object
+ pull_request = MagicMock()
+ # Create mock comments
+ comments = [
+ MagicMock(body="This is a comment that does not match any failure or pass condition"),
+ ]
+ # Set the return value of get_issue_comments to the mock comments
+ pull_request.get_issue_comments.return_value = comments
+
+ # Test the function
+ result, comment = has_check_previously_passed_or_failed(pull_request)
+
+ assert result == False
+ assert comment is None
+
+
+def test_has_check_previously_passed_or_failed_passed_status():
+ # Create a mock PullRequest object
+ pull_request = MagicMock()
+ # Create mock comments
+ comments = [
+ MagicMock(body="This is a comment that does not match any failure or pass condition"),
+ MagicMock(body="The committers listed above are authorized under a signed CLA."),
+ ]
+ # Set the return value of get_issue_comments to the mock comments
+ pull_request.get_issue_comments.return_value = comments
+
+ # Test the function
+ result, comment = has_check_previously_passed_or_failed(pull_request)
+
+ # Assert that the function returns True and the correct comment
+ assert result
+ assert comment == comments[1]
+
+
class TestWebhookSecretValidation(unittest.TestCase):
def setUp(self) -> None:
self.old_email = cla.config.EMAIL_SERVICE
@@ -133,21 +171,21 @@ def test_webhook_secret_validation_empty(self):
"""
cla.config.GITHUB_APP_WEBHOOK_SECRET = ""
with self.assertRaises(RuntimeError) as ex:
- _ = webhook_secret_validation("secret", b'')
+ _ = webhook_secret_validation("secret", b"")
def test_webhook_secret_validation_failed(self):
"""
Tests the webhook_secret_validation method
"""
cla.config.GITHUB_APP_WEBHOOK_SECRET = "secret"
- assert not webhook_secret_validation("sha1=secret", ''.encode())
+ assert not webhook_secret_validation("sha1=secret", "".encode())
def test_webhook_secret_validation_success(self):
"""
Tests the webhook_secret_validation method
"""
cla.config.GITHUB_APP_WEBHOOK_SECRET = "secret"
- input_data = 'data'.encode('utf-8')
+ input_data = "data".encode("utf-8")
assert webhook_secret_validation("sha1=9818e3306ba5ac267b5f2679fe4abd37e6cd7b54", input_data)
# def test_webhook_secret_failed_email(self):
diff --git a/cla-backend/cla/tests/unit/test_github_controller.py b/cla-backend/cla/tests/unit/test_github_controller.py
index b8ccad9ec..26fb2d71d 100644
--- a/cla-backend/cla/tests/unit/test_github_controller.py
+++ b/cla-backend/cla/tests/unit/test_github_controller.py
@@ -5,8 +5,9 @@
from unittest.mock import Mock
import cla
-from cla.controllers.github import get_org_name_from_installation_event, get_github_activity_action, \
- notify_project_managers
+from cla.controllers.github import (get_github_activity_action,
+ get_org_name_from_installation_event,
+ notify_project_managers)
from cla.controllers.repository import Repository
from cla.models.ses_models import MockSES
diff --git a/cla-backend/cla/tests/unit/test_github_models.py b/cla-backend/cla/tests/unit/test_github_models.py
index 994fc124d..6df0c02c5 100644
--- a/cla-backend/cla/tests/unit/test_github_models.py
+++ b/cla-backend/cla/tests/unit/test_github_models.py
@@ -1,146 +1,95 @@
# Copyright The Linux Foundation and each contributor to CommunityBridge.
# SPDX-License-Identifier: MIT
-import logging
-import unittest
-from unittest.mock import Mock, patch, MagicMock
-
-from github import Github
-
-import cla
-from cla.models.github_models import get_pull_request_commit_authors, handle_commit_from_user, MockGitHub
-from cla.models.dynamo_models import Signature, Project
-
-
-class TestGitHubModels(unittest.TestCase):
-
- @classmethod
- def setUpClass(cls) -> None:
- cls.mock_user_patcher = patch('cla.models.github_models.cla.utils.get_user_instance')
- cls.mock_signature_patcher = patch('cla.models.github_models.cla.utils.get_signature_instance')
- cls.mock_utils_patcher = patch('cla.models.github_models.cla.utils')
- cls.mock_utils_get = cls.mock_utils_patcher.start()
- cls.mock_user_get = cls.mock_user_patcher.start()
- cls.mock_signature_get = cls.mock_signature_patcher.start()
-
- @classmethod
- def tearDownClass(cls) -> None:
- cls.mock_user_patcher.stop()
- cls.mock_signature_patcher.stop()
- cls.mock_utils_patcher.stop()
-
- def setUp(self) -> None:
- # Only show critical logging stuff
- cla.log.level = logging.CRITICAL
- self.assertTrue(cla.conf['GITHUB_OAUTH_TOKEN'] != '',
- 'Missing GITHUB_OAUTH_TOKEN environment variable - required to run unit tests')
- # cla.log.debug('Using GITHUB_OAUTH_TOKEN: {}...'.format(cla.conf['GITHUB_OAUTH_TOKEN'][:5]))
-
- def tearDown(self) -> None:
- pass
-
- def test_commit_authors_with_named_user(self) -> None:
- """
- Test that we can load commit authors from a pull request that does have the traditional
- github.NamedUser.NamedUser object filled out
- """
- g = Github(cla.conf['GITHUB_OAUTH_TOKEN'])
- repo = g.get_repo(27729926) # grpc/grpc-java
- pr = repo.get_pull(6142) # example: https://github.com/grpc/grpc-java/pull/6142
- cla.log.info("Retrieved GitHub PR: {}".format(pr))
- commits = pr.get_comments()
- cla.log.info("Retrieved GitHub PR: {}, commits: {}".format(pr, commits))
-
- # Returns a list tuples, which look like (commit_sha_string, (author_id, author_username, author_email),
- # which, as you can see, the second element of the tuple is another tuple containing the author information
- commit_authors = get_pull_request_commit_authors(pr)
- # cla.log.info("Result: {}".format(commit_authors))
- # cla.log.info([author_info[1] for commit, author_info in commit_authors])
- self.assertTrue(4779759 in [author_info[0] for commit, author_info in commit_authors])
-
- def test_commit_authors_no_named_user(self) -> None:
- """
- Test that we can load commit authors from a pull request that does NOT have the traditional
- github.NamedUser.NamedUser object filled out
- """
- # We need to mock this service so that we can test our business logic - disabling this test for now
- # as they closed the PR
- g = Github(cla.conf['GITHUB_OAUTH_TOKEN'])
- repo = g.get_repo(27729926) # grpc/grpc-java
- pr = repo.get_pull(6152) # example: https://github.com/grpc/grpc-java/pull/6152
- cla.log.info("Retrieved GitHub PR: {}".format(pr))
- commits = pr.get_comments()
- cla.log.info("Retrieved GitHub PR: {}, commits: {}".format(pr, commits))
-
- # Returns a list tuples, which look like (commit_sha_string, (author_id, author_username, author_email),
- # which, as you can see, the second element of the tuple is another tuple containing the author information
- # commit_authors = get_pull_request_commit_authors(pr)
- # cla.log.info("Result: {}".format(commit_authors))
- # cla.log.info([author_info[1] for commit, author_info in commit_authors])
- # self.assertTrue('snalkar' in [author_info[1] for commit, author_info in commit_authors])
- def test_handle_commit_author_whitelisted(self) -> None:
- """
- Test case where commit authors have no signatures but have been whitelisted and should
- return missing list containing a whitelisted flag
- """
- # Mock user not existing and happens to be whitelisted
- self.mock_user_get.return_value.get_user_by_github_id.return_value = None
- self.mock_user_get.return_value.get_user_by_email.return_value = None
- self.mock_signature_get.return_value.get_signatures_by_project.return_value = [Signature()]
- self.mock_utils_get.return_value.is_approved.return_value = True
- missing = []
- signed = []
- project = Project()
- project.set_project_id('fake_project_id')
- handle_commit_from_user(project, 'fake_sha', (123, 'foo', 'foo@gmail.com'), signed, missing)
- # We commented out this functionality for now - re-enable if we add it back
- # self.assertListEqual(missing, [('fake_sha', [123, 'foo', 'foo@gmail.com', True])])
- self.assertEqual(signed, [])
-
-
-class TestGithubModelsPrComment(unittest.TestCase):
-
- def setUp(self) -> None:
- self.github = MockGitHub()
- self.github.update_change_request = MagicMock()
-
- def tearDown(self) -> None:
- pass
-
- def test_process_easycla_command_comment(self):
- with self.assertRaisesRegex(ValueError, "missing comment body"):
- self.github.process_easycla_command_comment({})
-
- with self.assertRaisesRegex(ValueError, "unsupported comment supplied"):
- self.github.process_easycla_command_comment({
- "comment": {"body": "/otherbot"}
- })
-
- with self.assertRaisesRegex(ValueError, "missing github repository id"):
- self.github.process_easycla_command_comment({
- "comment": {"body": "/easycla"},
- })
-
- with self.assertRaisesRegex(ValueError, "missing pull request id"):
- self.github.process_easycla_command_comment({
- "comment": {"body": "/easycla"},
- "repository": {"id": 123},
- })
-
- with self.assertRaisesRegex(ValueError, "missing installation id"):
- self.github.process_easycla_command_comment({
- "comment": {"body": "/easycla"},
- "repository": {"id": 123},
- "issue": {"number": 1},
- })
-
- self.github.process_easycla_command_comment({
- "comment": {"body": "/easycla"},
- "repository": {"id": 123},
- "issue": {"number": 1},
- "installation": {"id": 1},
- })
-
-
-if __name__ == '__main__':
+import unittest
+from unittest import TestCase
+from unittest.mock import MagicMock, Mock, patch
+
+from cla.models.github_models import (UserCommitSummary, get_author_summary,
+ get_co_author_commits,
+ get_pull_request_commit_authors)
+
+
+class TestGetPullRequestCommitAuthors(TestCase):
+ # @patch("cla.utils.get_repository_service")
+ # def test_get_pull_request_commit_with_co_author(self, mock_github_instance):
+ # # Mock data
+ # pull_request = MagicMock()
+ # pull_request.number = 123
+ # co_author = "co_author"
+ # co_author_email = "co_author_email.gmail.com"
+ # co_author_2 = "co_author_2"
+ # co_author_email_2 = "co_author_email_2.gmail.com"
+ # commit = MagicMock()
+ # commit.sha = "fake_sha"
+ # commit.author = MagicMock()
+ # commit.author.id = 1
+ # commit.author.login = "fake_login"
+ # commit.author.name = "Fake Author"
+ # commit.commit.message = f"fake message\n\nCo-authored-by: {co_author} <{co_author_email}>\n\nCo-authored-by: {co_author_2} <{co_author_email_2}>"
+
+ # commit.author.email = "fake_author@example.com"
+ # pull_request.get_commits.return_value.__iter__.return_value = [commit]
+
+ # mock_user = Mock(id=2, login="co_author_login")
+ # mock_user_2 = Mock(id=3, login="co_author_login_2")
+
+ # mock_github_instance.return_value.get_github_user_by_email.side_effect = (
+ # lambda email, _: mock_user if email == co_author_email else mock_user_2
+ # )
+
+ # # Call the function
+ # result = get_pull_request_commit_authors(pull_request, "fake_installation_id")
+
+ # # Assertions
+ # self.assertEqual(len(result), 3)
+ # self.assertIn(co_author_email, [author.author_email for author in result])
+ # self.assertIn(co_author_email_2, [author.author_email for author in result])
+ # self.assertIn("fake_login", [author.author_login for author in result])
+ # self.assertIn("co_author_login", [author.author_login for author in result])
+
+ @patch("cla.utils.get_repository_service")
+ def test_get_co_author_commits_invalid_gh_email(self, mock_github_instance):
+ # Mock data
+ co_author = ("co_author", "co_author_email.gmail.com")
+ commit = MagicMock()
+ commit.sha = "fake_sha"
+ mock_github_instance.return_value.get_github_user_by_email.return_value = None
+ pr = 1
+ installation_id = 123
+
+ # Call the function
+ result = get_co_author_commits(co_author,commit, pr, installation_id)
+
+ # Assertions
+ self.assertEqual(result.commit_sha, "fake_sha")
+ self.assertEqual(result.author_id, None)
+ self.assertEqual(result.author_login, None)
+ self.assertEqual(result.author_email, "co_author_email.gmail.com")
+ self.assertEqual(result.author_name, "co_author")
+
+ @patch("cla.utils.get_repository_service")
+ def test_get_co_author_commits_valid_gh_email(self, mock_github_instance):
+ # Mock data
+ co_author = ("co_author", "co_author_email.gmail.com")
+ commit = MagicMock()
+ commit.sha = "fake_sha"
+ mock_github_instance.return_value.get_github_user_by_email.return_value = Mock(
+ id=123, login="co_author_login"
+ )
+ pr = 1
+ installation_id = 123
+
+ # Call the function
+ result = get_co_author_commits(co_author,commit, pr, installation_id)
+
+ # Assertions
+ self.assertEqual(result.commit_sha, "fake_sha")
+ self.assertEqual(result.author_id, 123)
+ self.assertEqual(result.author_login, "co_author_login")
+ self.assertEqual(result.author_email, "co_author_email.gmail.com")
+ self.assertEqual(result.author_name, "co_author")
+
+
+if __name__ == "__main__":
unittest.main()
diff --git a/cla-backend/cla/tests/unit/test_gitlab_org_models.py b/cla-backend/cla/tests/unit/test_gitlab_org_models.py
new file mode 100644
index 000000000..e0c3ed85a
--- /dev/null
+++ b/cla-backend/cla/tests/unit/test_gitlab_org_models.py
@@ -0,0 +1,40 @@
+# Copyright The Linux Foundation and each contributor to CommunityBridge.
+# SPDX-License-Identifier: MIT
+
+from cla.models.dynamo_models import GitlabOrg
+
+
+def test_gitlab_org_model():
+ gitlab_org = GitlabOrg(organization_name="GitlabOrg1")
+ assert gitlab_org.get_organization_id()
+ assert gitlab_org.get_organization_name() == "GitlabOrg1"
+ assert gitlab_org.get_organization_name_lower() == "gitlaborg1"
+ assert not gitlab_org.get_auto_enabled()
+ assert gitlab_org.get_enabled()
+ assert not gitlab_org.get_branch_protection_enabled()
+ assert not gitlab_org.get_project_sfid()
+ assert not gitlab_org.get_organization_sfid()
+
+ gitlab_org.set_organization_name("GitlabOrg2")
+ assert gitlab_org.get_organization_name() == "GitlabOrg2"
+ assert gitlab_org.get_organization_name_lower() == "gitlaborg2"
+
+ gitlab_org.set_enabled(False)
+ assert not gitlab_org.get_enabled()
+
+ gitlab_org.set_project_sfid("project_sfid_1")
+ assert gitlab_org.get_project_sfid() == "project_sfid_1"
+
+ gitlab_org.set_organization_sfid("organization_sfid_1")
+ assert gitlab_org.get_organization_sfid() == "organization_sfid_1"
+
+ gitlab_org.set_branch_protection_enabled(True)
+ assert gitlab_org.get_branch_protection_enabled()
+ gitlab_org.set_auto_enabled(True)
+ assert gitlab_org.get_auto_enabled()
+
+ gitlab_org_dict = gitlab_org.to_dict()
+ assert gitlab_org_dict["organization_id"] == gitlab_org.get_organization_id()
+ assert gitlab_org_dict["organization_name"] == "GitlabOrg2"
+ assert gitlab_org_dict["project_sfid"] == "project_sfid_1"
+ assert gitlab_org_dict["organization_sfid"] == "organization_sfid_1"
diff --git a/cla-backend/cla/tests/unit/test_model.py b/cla-backend/cla/tests/unit/test_model.py
index 17ef7751c..611567a19 100644
--- a/cla-backend/cla/tests/unit/test_model.py
+++ b/cla-backend/cla/tests/unit/test_model.py
@@ -4,17 +4,16 @@
"""
Test python API changes for Signature and User Tables
"""
-from unittest.mock import patch, MagicMock
+from unittest.mock import MagicMock, patch
+import cla
import pytest
-
-from cla.models.dynamo_models import SignatureModel, UserModel
-from cla.utils import get_user_instance, get_signature_instance, get_company_instance
from cla import utils
+from cla.models.dynamo_models import SignatureModel, UserModel
from cla.tests.unit.data import USER_TABLE_DATA
-
+from cla.utils import (get_company_instance, get_signature_instance,
+ get_user_instance)
from pynamodb.indexes import AllProjection
-import cla
PATCH_METHOD = "pynamodb.connection.Connection._make_api_call"
@@ -27,38 +26,6 @@ def test_company_external_id(company_instance):
assert "external id: external id" in str(company_instance)
-def test_signature_project_external_id(signature_instance):
- assert "signature project external id: proj_id" in str(signature_instance)
-
-
-def test_signature_company_signatory_id(signature_instance):
- assert "signature company signatory id: comp_sig_id" in str(signature_instance)
-
-
-def test_signature_company_signatory_name(signature_instance):
- assert "signature company signatory name: name" in str(signature_instance)
-
-
-def test_signature_company_signatory_email(signature_instance):
- assert "signature company signatory email: email" in str(signature_instance)
-
-
-def test_signature_company_initial_manager_id(signature_instance):
- assert "signature company initial manager id: manager_id" in str(signature_instance)
-
-
-def test_signature_company_initial_manager_name(signature_instance):
- assert "signature company initial manager name: manager_name" in str(signature_instance)
-
-
-def test_signature_company_initial_manager_email(signature_instance):
- assert "signature company initial manager email: manager_email" in str(signature_instance)
-
-
-def test_signature_company_secondary_manager_list(signature_instance):
- assert "signature company secondary manager list: {'foo': 'bar'}" in str(signature_instance)
-
-
def test_github_user_external_id_index():
assert UserModel.github_user_external_id_index.query("foo")
diff --git a/cla-backend/cla/tests/unit/test_project_event.py b/cla-backend/cla/tests/unit/test_project_event.py
index 82f88b30d..d26990abe 100644
--- a/cla-backend/cla/tests/unit/test_project_event.py
+++ b/cla-backend/cla/tests/unit/test_project_event.py
@@ -1,21 +1,17 @@
# Copyright The Linux Foundation and each contributor to CommunityBridge.
# SPDX-License-Identifier: MIT
-import os
-from unittest.mock import MagicMock, Mock, patch
-
-import pytest
-from falcon import HTTP_200
-from pynamodb.tests.deep_eq import deep_eq
+from unittest.mock import Mock, patch
import cla
from cla.auth import AuthUser
from cla.controllers import project as project_controller
-from cla.models.dynamo_models import Project, User, Document, UserPermissions, Event
+from cla.models.dynamo_models import Document, Project, User, UserPermissions
from cla.models.event_types import EventType
PATCH_METHOD = "pynamodb.connection.Connection._make_api_call"
+
@patch('cla.controllers.project.Event.create_event')
def test_event_delete_project(mock_event, project):
""" Test Delete Project event """
@@ -33,12 +29,13 @@ def test_event_delete_project(mock_event, project):
# Check whether audit event service is invoked
mock_event.assert_called_with(
event_type=expected_event_type,
- event_project_id=project_id,
+ event_cla_group_id=project_id,
event_data=expected_event_data,
event_summary=expected_event_data,
contains_pii=False,
)
+
@patch('cla.controllers.project.Event.create_event')
def test_event_create_project(mock_event):
""" Test Create Project event """
@@ -70,12 +67,14 @@ def test_event_create_project(mock_event):
# Test for audit event
mock_event.assert_called_with(
event_type=event_type,
- event_project_id=project_id,
+ event_cla_group_id=project_id,
+ event_project_id=project_external_id,
event_data=expected_event_data,
event_summary=expected_event_data,
contains_pii=False,
)
+
@patch('cla.controllers.project.Event.create_event')
def test_event_update_project(mock_event, project):
""" Test Update Project event """
@@ -101,10 +100,11 @@ def test_event_update_project(mock_event, project):
event_type=event_type,
event_data=expected_event_data,
event_summary=expected_event_data,
- event_project_id=project_id,
+ event_cla_group_id=project_id,
contains_pii=False,
)
+
@patch('cla.controllers.project.Event.create_event')
def test_create_project_document(mock_event, project):
""" Test create project Document event """
@@ -139,9 +139,14 @@ def test_create_project_document(mock_event, project):
new_major_version=False,
)
mock_event.assert_called_with(
- event_type=event_type, event_project_id=project_id, event_data=event_data, event_summary=event_data, contains_pii=False,
+ event_type=event_type,
+ event_cla_group_id=project_id,
+ event_data=event_data,
+ event_summary=event_data,
+ contains_pii=False,
)
+
@patch('cla.controllers.project.Event.create_event')
def test_create_project_document_template(mock_event, project):
""" Test creating project document with existing template event """
@@ -178,9 +183,14 @@ def test_create_project_document_template(mock_event, project):
)
mock_event.assert_called_with(
- event_type=event_type, event_project_id=project_id, event_data=event_data, event_summary=event_data, contains_pii=False,
+ event_type=event_type,
+ event_cla_group_id=project_id,
+ event_data=event_data,
+ event_summary=event_data,
+ contains_pii=False,
)
+
@patch('cla.controllers.project.Event.create_event')
def test_delete_project_document(mock_event):
""" Test event for deleting document from the specified project """
@@ -193,8 +203,8 @@ def test_delete_project_document(mock_event):
major_version = "v1"
minor_version = "v1"
event_data = (
- f'Project {project.get_project_name()} with {document_type} :'
- +f'document type , minor version : {minor_version}, major version : {major_version} deleted'
+ f'Project {project.get_project_name()} with {document_type} :'
+ + f'document type , minor version : {minor_version}, major version : {major_version} deleted'
)
Project.load = Mock()
@@ -208,21 +218,26 @@ def test_delete_project_document(mock_event):
)
mock_event.assert_called_with(
- event_type=event_type, event_project_id=project_id, event_data=event_data, event_summary=event_data, contains_pii = False,
+ event_type=event_type,
+ event_cla_group_id=project_id,
+ event_data=event_data,
+ event_summary=event_data,
+ contains_pii=False,
)
+
@patch('cla.controllers.project.Event.create_event')
def test_project_add_permission_existing_user(mock_event, project):
""" Test adding permissions to project event """
auth_claims = {
'auth0_username_claim': 'http:/localhost/foo',
'email': 'foo@gmail.com',
- 'sub' : 'bar',
- 'name' : 'name'
+ 'sub': 'bar',
+ 'name': 'name'
}
username = 'harry'
auth_user = AuthUser(auth_claims)
- auth_user.username='ddeal'
+ auth_user.username = 'ddeal'
event_type = EventType.AddPermission
project_sfdc_id = 'project_sfdc_id'
@@ -253,12 +268,12 @@ def test_project_remove_permission(mock_event):
auth_claims = {
'auth0_username_claim': 'http:/localhost/foo',
'email': 'foo@gmail.com',
- 'sub' : 'bar',
- 'name' : 'name'
+ 'sub': 'bar',
+ 'name': 'name'
}
username = 'harry'
auth_user = AuthUser(auth_claims)
- auth_user.username='ddeal'
+ auth_user.username = 'ddeal'
event_type = EventType.RemovePermission
project_sfdc_id = 'project_sfdc_id'
@@ -282,6 +297,7 @@ def test_project_remove_permission(mock_event):
contains_pii=True,
)
+
@patch('cla.controllers.project.Event.create_event')
def test_add_project_manager(mock_event, project):
""" Tests event logging where LFID is added to the project ACL """
@@ -302,22 +318,23 @@ def test_add_project_manager(mock_event, project):
project.get_project_id(),
lfid
)
- event_data = '{} added {} to project {}'.format(username,lfid,project.get_project_name())
+ event_data = '{} added {} to project {}'.format(username, lfid, project.get_project_name())
mock_event.assert_called_with(
event_type=event_type,
event_data=event_data,
event_summary=event_data,
- event_project_id=project.get_project_id(),
+ event_cla_group_id=project.get_project_id(),
contains_pii=True,
)
+
@patch('cla.controllers.project.Event.create_event')
def test_remove_project_manager(mock_event, project):
""" Test event logging where lfid is removed from the project acl """
event_type = EventType.RemoveProjectManager
Project.load = Mock()
- Project.get_project_acl = Mock(return_value=('foo','bar'))
+ Project.get_project_acl = Mock(return_value=('foo', 'bar'))
Project.remove_project_acl = Mock()
Project.save = Mock()
@@ -331,6 +348,6 @@ def test_remove_project_manager(mock_event, project):
event_type=event_type,
event_data=event_data,
event_summary=event_data,
- event_project_id=project.get_project_id(),
+ event_cla_group_id=project.get_project_id(),
contains_pii=True,
- )
\ No newline at end of file
+ )
diff --git a/cla-backend/cla/tests/unit/test_salesforce_projects.py b/cla-backend/cla/tests/unit/test_salesforce_projects.py
index 5c2d21779..ebc8315e1 100644
--- a/cla-backend/cla/tests/unit/test_salesforce_projects.py
+++ b/cla-backend/cla/tests/unit/test_salesforce_projects.py
@@ -4,13 +4,12 @@
import json
import os
from http import HTTPStatus
-from unittest.mock import Mock, patch, MagicMock
-
-import pytest
+from unittest.mock import MagicMock, Mock, patch
import cla
+import pytest
from cla.models.dynamo_models import UserPermissions
-from cla.salesforce import get_projects, get_project
+from cla.salesforce import get_project, get_projects
@pytest.fixture()
diff --git a/cla-backend/cla/tests/unit/test_signature_controller.py b/cla-backend/cla/tests/unit/test_signature_controller.py
index 975b78ec4..4544d626e 100644
--- a/cla-backend/cla/tests/unit/test_signature_controller.py
+++ b/cla-backend/cla/tests/unit/test_signature_controller.py
@@ -7,7 +7,7 @@
import cla
from cla.controllers.signature import notify_whitelist_change
from cla.controllers.signing import canceled_signature_html
-from cla.models.dynamo_models import User, Signature, Project
+from cla.models.dynamo_models import Project, Signature, User
from cla.models.sns_email_models import MockSNS
from cla.user import CLAUser
diff --git a/cla-backend/cla/tests/unit/test_signature_event.py b/cla-backend/cla/tests/unit/test_signature_event.py
deleted file mode 100644
index 9c5a428ee..000000000
--- a/cla-backend/cla/tests/unit/test_signature_event.py
+++ /dev/null
@@ -1,146 +0,0 @@
- # Copyright The Linux Foundation and each contributor to CommunityBridge.
-# SPDX-License-Identifier: MIT
-
-from unittest.mock import patch, Mock
-
-import pytest
-
-from cla.models.dynamo_models import Signature,Project,Company, Document
-from cla.controllers import signature as signature_controller
-from cla.controllers import company
-from cla.models.event_types import EventType
-from cla.auth import AuthUser
-
-
-
-@pytest.fixture()
-def create_event_signature():
- signature_controller.create_event = Mock()
-
-@pytest.fixture()
-def auth_user():
- with patch.object(AuthUser, "__init__", lambda self: None):
- user = AuthUser()
- yield user
-
-@patch('cla.controllers.signature.Event.create_event')
-def test_create_signature(mock_event, create_event_signature, project):
- """ Test create signature event """
- Project.load = Mock()
- Company.load = Mock()
- Signature.set_signature_document_major_version = Mock()
- Signature.set_signature_document_minor_version = Mock()
- Project.get_project_corporate_document = Mock()
- Project.get_project_name = Mock(return_value=project.get_project_name())
- Signature.save = Mock()
- event_type = EventType.CreateSignature
- signature_id = 'new_signature_id'
- Signature.get_signature_id = Mock(return_value=signature_id)
- project_id = project.get_project_id()
- project = project.get_project_name()
- event_data = f'Signature added. Signature_id - {signature_id} for Project - {project}'
- signature_controller.create_signature(
- project_id,'signature_reference_id','signature_reference_type'
- )
- mock_event.assert_called_once_with(
- event_data=event_data,
- event_summary=event_data,
- event_type=event_type,
- event_project_id=project_id,
- contains_pii=False,
- )
-
-@patch('cla.controllers.signature.Event.create_event')
-def test_update_signature(mock_event, auth_user, create_event_signature, signature_instance):
- """ Test update signature """
- Signature.load = Mock()
- auth_user.name = 'ddeal'
- event_type = EventType.UpdateSignature
- signature_controller.notify_whitelist_change = Mock()
- # test signature_reference_type_check
- signature_controller.update_signature(
- signature_instance.get_signature_id(),
- auth_user,
- signature_reference_type='type'
- )
-
- event_data = f'signature {signature_instance.get_signature_id()} updates: \n signature_reference_type updated to type \n'
- mock_event.assert_called_once_with(
- event_data=event_data,
- event_summary=event_data,
- event_type=event_type,
- contains_pii=True,
- )
-
-@patch('cla.controllers.signature.Event.create_event')
-def test_delete_signature(mock_event, create_event_signature, signature_instance):
- """ Test delete signature """
- event_type = EventType.DeleteSignature
- event_data = f'Deleted signature {signature_instance.get_signature_id()}'
- signature_controller.delete_signature(
- signature_instance.get_signature_id()
- )
- mock_event.assert_called_once_with(
- event_data=event_data,
- event_summary=event_data,
- event_type=event_type,
- contains_pii=False,
- )
-
-@patch('cla.controllers.signature.Event.create_event')
-def test_add_cla_manager(mock_event, auth_user, signature_instance, create_event_signature):
- """ Test add cla manager event """
- Signature.load = Mock()
- auth_user.username = 'harold'
- Signature.get_signature_acl = Mock(return_value=('harold'))
- company.add_permission = Mock()
- Signature.add_signature_acl = Mock()
- Signature.save = Mock()
- signature_controller.get_managers_dict = Mock()
- lfid = 'foo_lfid'
- subject = 'Add CLA Manager'
- body = 'Added %s' % lfid
- recipients = ['foo@gmail.com']
- signature_controller.add_cla_manager_email_content = Mock(return_value=(subject, body, recipients))
- signature_controller.get_email_service = Mock()
- signature_controller.add_cla_manager(
- auth_user,
- signature_instance.get_signature_id(),
- lfid
- )
- event_data = f'{lfid} added as cla manager to Signature ACL for {signature_instance.get_signature_id()}'
-
- mock_event.assert_called_once_with(
- event_data=event_data,
- event_summary=event_data,
- event_type=EventType.AddCLAManager,
- contains_pii=True,
- )
-
-@patch('cla.controllers.signature.Event.create_event')
-def test_remove_cla_manager(mock_event, signature_instance, create_event_signature):
- """ Test remove cla_manager """
- Signature.get_signature_acl = Mock(return_value=('harold'))
- Signature.load = Mock()
- Signature.remove_signature_acl = Mock()
- Signature.save = Mock()
- signature_controller.get_managers_dict = Mock()
- event_type = EventType.RemoveCLAManager
- lfid = 'nachwera'
- subject = 'Removed CLA Manager'
- body = 'Removed %s' %lfid
- recipients = ['foo@gmail.com']
- signature_controller.remove_cla_manager_email_content = Mock(return_value=(subject, body, recipients))
- signature_controller.get_email_service = Mock()
- signature_controller.remove_cla_manager(
- 'harold', signature_instance.get_signature_id(), lfid
- )
- event_data = f'User with lfid {lfid} removed from project ACL with signature {signature_instance.get_signature_id()}'
- mock_event.assert_called_once_with(
- event_data=event_data,
- event_summary=event_data,
- event_type=event_type,
- contains_pii=True,
- )
-
-
diff --git a/cla-backend/cla/tests/unit/test_user_commit_summary.py b/cla-backend/cla/tests/unit/test_user_commit_summary.py
new file mode 100644
index 000000000..75f759ed4
--- /dev/null
+++ b/cla-backend/cla/tests/unit/test_user_commit_summary.py
@@ -0,0 +1,63 @@
+# Copyright The Linux Foundation and each contributor to CommunityBridge.
+# SPDX-License-Identifier: MIT
+
+import unittest
+
+from cla.user import UserCommitSummary
+from cla.utils import get_comment_body
+
+
+class TestUserCommitSummary(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls) -> None:
+ pass
+
+ @classmethod
+ def tearDownClass(cls) -> None:
+ pass
+
+ def setUp(self) -> None:
+ pass
+
+ def tearDown(self) -> None:
+ pass
+
+ def test_user_commit_summary_is_valid(self) -> None:
+ t = UserCommitSummary("some_sha", 1234, 'login_value', 'author name', 'foo@bar.com', False, False)
+ self.assertTrue(t.is_valid_user())
+ t = UserCommitSummary("some_sha", 1234, None, None, 'foo@bar.com', False, False)
+ self.assertFalse(t.is_valid_user())
+
+ def test_user_commit_summary_get_comment_body(self) -> None:
+ s1 = UserCommitSummary("abc1234xyz-123", 1234, 'login_value', 'author name', 'foo@bar.com', True, True)
+ s2 = UserCommitSummary("abc1234xyz-456", 1234, 'login_value', 'author name', 'foo@bar.com', True, True)
+ signed = [s1, s2]
+
+ m = UserCommitSummary("some_other_sha", 123456, 'login_value2', 'author name2', 'foo2@bar.com', False, False)
+ missing = [m]
+
+ body = get_comment_body('github', 'https://foo.com', signed, missing)
+ self.assertTrue(':white_check_mark:' in body)
+ self.assertTrue(':x:' in body)
+
+ def test_user_commit_summary_tag_not_in_get_comment_body(self) -> None:
+ s1 = UserCommitSummary("abc1234xyz-123", 1234, 'login_value', 'author name', 'foo@bar.com', True, True)
+ s2 = UserCommitSummary("abc1234xyz-456", 1234, 'login_value', 'author name', 'foo@bar.com', True, True)
+ signed = [s1, s2]
+
+ missing = []
+
+ body = get_comment_body('github', 'https://foo.com', signed, missing)
+ self.assertTrue(':white_check_mark:' in body)
+ self.assertTrue('login_value' in body)
+ self.assertFalse('@login_value' in body) # users should not be tagged in signed use case
+
+ def test_user_commit_summary_tag_in_get_comment_body(self) -> None:
+ signed = []
+
+ m = UserCommitSummary("some_other_sha", 123456, 'login_value2', 'author name2', 'foo2@bar.com', False, False)
+ missing = [m]
+
+ body = get_comment_body('github', 'https://foo.com', signed, missing)
+ self.assertTrue(':x:' in body)
+ self.assertTrue('@login_value2' in body) # users should be tagged in missing use case
diff --git a/cla-backend/cla/tests/unit/test_user_event.py b/cla-backend/cla/tests/unit/test_user_event.py
index 09d37531d..01aee68a7 100644
--- a/cla-backend/cla/tests/unit/test_user_event.py
+++ b/cla-backend/cla/tests/unit/test_user_event.py
@@ -1,15 +1,13 @@
# Copyright The Linux Foundation and each contributor to CommunityBridge.
# SPDX-License-Identifier: MIT
-from unittest.mock import patch, Mock
-import unittest
+from unittest.mock import Mock, patch
import pytest
-
-from cla.models.dynamo_models import User, Project, Company, CCLAWhitelistRequest, CompanyInvite
-from cla.models.event_types import EventType
from cla.controllers import user as user_controller
-from cla.auth import AuthUser
+from cla.models.dynamo_models import (CCLAWhitelistRequest, Company,
+ CompanyInvite, Project, User)
+from cla.models.event_types import EventType
@pytest.fixture
@@ -17,7 +15,7 @@ def create_event_user():
user_controller.create_event = Mock()
-class TestRequestCompanyWhitelist:
+class TestRequestCompanyApprovalList:
def setup(self) -> None:
self.old_load = User.load
@@ -43,7 +41,7 @@ def teardown(self) -> None:
Project.load = self.project_load
Project.get_project_name = self.get_project_name
- def test_request_company_whitelist(self, create_event_user, project, company, user):
+ def test_request_company_approval_list(self, create_event_user, project, company, user):
""" Test user requesting to be added to the Approved List event """
with patch('cla.controllers.user.Event.create_event') as mock_event:
event_type = EventType.RequestCompanyWL
@@ -55,6 +53,7 @@ def test_request_company_whitelist(self, create_event_user, project, company, us
Company.get_company_name = Mock(return_value=company.get_company_name())
Project.load = Mock()
Project.get_project_name = Mock(return_value=project.get_project_name())
+ Project.get_project_id = Mock(return_value=project.get_project_id())
user_controller.get_email_service = Mock()
user_controller.send = Mock()
user_controller.request_company_whitelist(
@@ -75,7 +74,7 @@ def test_request_company_whitelist(self, create_event_user, project, company, us
mock_event.assert_called_once_with(
event_user_id=user.get_user_id(),
- event_project_id=project.get_project_id(),
+ event_cla_group_id=project.get_project_id(),
event_company_id=company.get_company_id(),
event_type=event_type,
event_data=event_data,
@@ -113,6 +112,7 @@ def test_invite_cla_manager(self, mock_event, create_event_user, user):
cla_manager_name = "admin"
cla_manager_email = "foo@admin.com"
project_name = "foo_project"
+ project_id = "foo_project_id"
company_name = "Test Company"
event_data = (f'sent email to CLA Manager: {cla_manager_name} with email {cla_manager_email} '
f'for project {project_name} and company {company_name} '
@@ -125,8 +125,9 @@ def test_invite_cla_manager(self, mock_event, create_event_user, user):
event_user_id=contributor_id,
event_project_name=project_name,
event_data=event_data,
- event_type=EventType.InviteAdmin,
event_summary=event_data,
+ event_type=EventType.InviteAdmin,
+ event_cla_group_id=project_id,
contains_pii=True,
)
@@ -158,6 +159,7 @@ def test_request_company_ccla(self, mock_event, create_event_user, user, project
Company.load = Mock()
Project.load = Mock()
Project.get_project_name = Mock(return_value=project.get_project_name())
+ Project.get_project_id = Mock(return_value=project.get_project_id())
manager = User(lf_username="harold", user_email="foo@gmail.com")
Company.get_managers = Mock(return_value=[manager, ])
event_data = f"Sent email to sign ccla for {project.get_project_name()}"
@@ -171,5 +173,6 @@ def test_request_company_ccla(self, mock_event, create_event_user, user, project
event_type=EventType.RequestCCLA,
event_user_id=user.get_user_id(),
event_company_id=company.get_company_id(),
+ event_cla_group_id=project.get_project_id(),
contains_pii=False,
)
diff --git a/cla-backend/cla/tests/unit/test_user_service.py b/cla-backend/cla/tests/unit/test_user_service.py
index e407a6fe7..eaa688980 100644
--- a/cla-backend/cla/tests/unit/test_user_service.py
+++ b/cla-backend/cla/tests/unit/test_user_service.py
@@ -1,56 +1,58 @@
# Copyright The Linux Foundation and each contributor to CommunityBridge.
# SPDX-License-Identifier: MIT
-from unittest.mock import patch
-
-import pytest
-
-from cla.user_service import UserService
-from cla.models.dynamo_models import ProjectCLAGroup
-
-
-@pytest.fixture
-def mock_pcg():
- pcg = ProjectCLAGroup()
- pcg.set_project_sfid('foo_project_sfid')
- pcg.set_foundation_sfid('foo_foundation_sfid')
- pcg.set_cla_group_id('foo_cla_group_id')
- yield pcg
-
-@patch('cla.user_service.ProjectCLAGroup.get_by_cla_group_id')
-@patch('cla.user_service.UserService._list_org_user_scopes')
-def test_user_has_role_scope(mock_user_scopes, mock_pcgs, mock_pcg):
- """ Check if given user has role scope """
- mock_user_scopes.return_value = {
- 'userroles': [
- {
- 'RoleScopes' : [
- {
- 'RoleID': 'foo_role_id',
- 'RoleName': 'cla-maanger',
- 'Scopes' : [
- {
- 'ObjectID' : 'foo_project_sfid|foo_company_sfid',
- 'ObjectName' : 'foo_project_name|foo_company_name',
- 'ObjectTypeID': 11,
- 'ObjectTypeName': 'project|organization',
- 'ScopeID': 'foo_scope_id'
- }
- ]
- }
- ],
- 'Contact' : {
- 'ID': 'foo_id',
- 'Username': 'foo_username',
- 'EmailAddress': 'foo@gmail.com',
- 'Name': 'foo',
- 'LogoURL': 'http://logo.com',
- }
- },
- ]
- }
- mock_pcgs.return_value = [mock_pcg]
- user_service = UserService
- assert user_service.has_role('foo_username', 'cla-manager', 'foo_company_sfid', 'foo_cla_group_id')
- assert user_service.has_role('foo_no_role','cla-manager', 'foo_company_sfid', 'foo_cla_group_id') == False
-
+# TODO - Need to mock this set of tests so that it doesn't require the real service
+# from unittest.mock import patch
+#
+# import pytest
+#
+# from cla.user_service import UserService
+# from cla.models.dynamo_models import ProjectCLAGroup
+#
+#
+# @pytest.fixture
+# def mock_pcg():
+# pcg = ProjectCLAGroup()
+# pcg.set_project_sfid('foo_project_sfid')
+# pcg.set_foundation_sfid('foo_foundation_sfid')
+# pcg.set_cla_group_id('foo_cla_group_id')
+# yield pcg
+#
+#
+# @patch('cla.user_service.ProjectCLAGroup.get_by_cla_group_id')
+# @patch('cla.user_service.UserService._list_org_user_scopes')
+# def test_user_has_role_scope(mock_user_scopes, mock_pcgs, mock_pcg):
+# """ Check if given user has role scope """
+# mock_user_scopes.return_value = {
+# 'userroles': [
+# {
+# 'RoleScopes' : [
+# {
+# 'RoleID': 'foo_role_id',
+# 'RoleName': 'cla-maanger',
+# 'Scopes' : [
+# {
+# 'ObjectID' : 'foo_project_sfid|foo_company_sfid',
+# 'ObjectName' : 'foo_project_name|foo_company_name',
+# 'ObjectTypeID': 11,
+# 'ObjectTypeName': 'project|organization',
+# 'ScopeID': 'foo_scope_id'
+# }
+# ]
+# }
+# ],
+# 'Contact' : {
+# 'ID': 'foo_id',
+# 'Username': 'foo_username',
+# 'EmailAddress': 'foo@gmail.com',
+# 'Name': 'foo',
+# 'LogoURL': 'http://logo.com',
+# }
+# },
+# ]
+# }
+# mock_pcgs.return_value = [mock_pcg]
+# user_service = UserService
+# assert user_service.has_role('foo_username', 'cla-manager', 'foo_company_sfid', 'foo_cla_group_id')
+# assert user_service.has_role('foo_no_role','cla-manager', 'foo_company_sfid', 'foo_cla_group_id') == False
+#
diff --git a/cla-backend/cla/tests/unit/test_utils.py b/cla-backend/cla/tests/unit/test_utils.py
index bfda0d026..1384e82b8 100644
--- a/cla-backend/cla/tests/unit/test_utils.py
+++ b/cla-backend/cla/tests/unit/test_utils.py
@@ -6,9 +6,10 @@
import cla
from cla import utils
-from cla.models.dynamo_models import Signature, User, Project
-from cla.utils import append_email_help_sign_off_content, get_email_help_content, get_email_sign_off_content, \
- get_full_sign_url, append_project_version_to_url
+from cla.models.dynamo_models import Project, Signature, User
+from cla.utils import (append_email_help_sign_off_content,
+ append_project_version_to_url, get_email_help_content,
+ get_email_sign_off_content, get_full_sign_url)
class TestUtils(unittest.TestCase):
@@ -192,38 +193,92 @@ def test_get_full_sign_url():
def test_append_project_version_to_url():
- url = "http://localhost:5000/v1/sign"
- url = append_project_version_to_url(address=url, project_version="v1")
+ original_url = "http://localhost:5000/v1/sign"
+ url = append_project_version_to_url(address=original_url, project_version="v1")
+ print(url)
assert "?version=1" in url
+ assert original_url in url
- url = "http://localhost:5000/v1/sign"
- url = append_project_version_to_url(address=url, project_version="v2")
+ original_url = "http://localhost:5000/v1/sign"
+ url = append_project_version_to_url(address=original_url, project_version="v2")
+ print(url)
assert "?version=2" in url
assert "http://localhost:5000/v1/sign?version=2" == url
+ assert original_url in url
- url = "http://localhost:5000/v1/sign"
- url = append_project_version_to_url(address=url, project_version=None)
+ original_url = "http://localhost:5000/v1/sign"
+ url = append_project_version_to_url(address=original_url, project_version=None)
+ print(url)
assert "?version=1" in url
+ assert original_url in url
- url = "http://localhost:5000/v1/sign"
- url = append_project_version_to_url(address=url, project_version="invalid")
+ original_url = "http://localhost:5000/v1/sign"
+ url = append_project_version_to_url(address=original_url, project_version="invalid")
+ print(url)
assert "?version=1" in url
+ assert original_url in url
- url = "http://localhost:5000/v1/sign?something=else"
- url = append_project_version_to_url(address=url, project_version="v2")
+ original_url = "http://localhost:5000/v1/sign?something=else"
+ url = append_project_version_to_url(address=original_url, project_version="v2")
+ print(url)
assert "version=2" in url
assert "something=else" in url
+ assert original_url in url
- url = "http://localhost:5000/v1/sign?version=1"
- url = append_project_version_to_url(address=url, project_version="v2")
+ original_url = "http://localhost:5000/v1/sign?version=1"
+ url = append_project_version_to_url(address=original_url, project_version="v2")
+ print(url)
assert "version=2" not in url
assert "version=1" in url
+ assert original_url in url
- url = "http://localhost:5000/v1/sign?something=else&version=1"
- url = append_project_version_to_url(address=url, project_version="v2")
+ original_url = "http://localhost:5000/v1/sign?something=else&version=1"
+ url = append_project_version_to_url(address=original_url, project_version="v2")
+ print(url)
assert "version=2" not in url
assert "version=1" in url
assert "something=else" in url
+ assert original_url in url
+
+ # try the weird case with # in url
+ original_url = "https://dev.lfcla.com/#/"
+ url = append_project_version_to_url(address=original_url, project_version="v2")
+ print(url)
+ assert "version=2" in url
+ assert "version=1" not in url
+ assert original_url in url
+
+ original_url = "https://dev.lfcla.com/#/"
+ url = append_project_version_to_url(address=original_url, project_version="")
+ print(url)
+ assert "version=1" in url
+ assert "version=2" not in url
+ assert original_url in url
+
+ original_url = "https://dev.lfcla.com/#/"
+ url = append_project_version_to_url(address=original_url, project_version=None)
+ print(url)
+ assert "version=1" in url
+ assert "version=2" not in url
+ assert original_url in url
+
+ original_url= "https://dev.lfcla.com/#/#/?something=else"
+ url = append_project_version_to_url(address=original_url, project_version="")
+ print(url)
+ assert "version=1" in url
+ assert "something=else" in url
+ assert "version=2" not in url
+ assert original_url in url
+
+ # check for crazier example ...
+ original_url = "https://dev.lfcla.com/1/#/2/#/3/#/?something=else&this=that"
+ url = append_project_version_to_url(address=original_url, project_version="")
+ print(url)
+ assert "version=1" in url
+ assert "something=else" in url
+ assert "this=that" in url
+ assert "version=2" not in url
+ assert original_url in url
if __name__ == '__main__':
diff --git a/cla-backend/cla/user.py b/cla-backend/cla/user.py
index e36298262..93ff2df50 100644
--- a/cla-backend/cla/user.py
+++ b/cla-backend/cla/user.py
@@ -5,10 +5,16 @@
user.py contains the user class and hug directive.
"""
+import re
+from dataclasses import dataclass
+from typing import Optional
+
from hug.directives import _built_in_directive
-import cla
from jose import jwt
+import cla
+
+
@_built_in_directive
def cla_user(default=None, request=None, **kwargs):
"""Returns the current logged in CLA user"""
@@ -18,7 +24,7 @@ def cla_user(default=None, request=None, **kwargs):
cla.log.error('Error reading headers')
return default
- bearer_token = headers.get('Authorization') or headers.get('AUTHORIZATION')
+ bearer_token = headers.get('Authorization') or headers.get('AUTHORIZATION')
if bearer_token is None:
cla.log.error('Error reading authorization header')
@@ -49,3 +55,50 @@ def __init__(self, data):
self.family_name = data.get('family_name', None)
self.email = data.get('email', None)
self.roles = data.get('realm_access', {}).get('roles', [])
+
+
+@dataclass
+class UserCommitSummary:
+ commit_sha: str
+ author_id: Optional[int] # numeric ID of the user
+ author_login: Optional[str] # login identifier of the user
+ author_name: Optional[str] # english name of the user, typically First name Last name format.
+ author_email: Optional[str] # public email address of the user
+ authorized: bool
+ affiliated: bool
+
+ def __str__(self) -> str:
+ return (f'User Commit Summary, '
+ f'commit SHA: {self.commit_sha}, '
+ f'author id: {self.author_id}, '
+ f'login: {self.author_login}, '
+ f'name: {self.author_name}, '
+ f'email: {self.author_email}.')
+
+ def is_valid_user(self) -> bool:
+ return self.author_id is not None and (self.author_login is not None or self.author_name is not None)
+
+ def get_user_info(self, tag_user: bool = False) -> str:
+ user_info = ''
+ if self.author_login:
+ user_info += f'login: {"@" if tag_user else ""}{self.author_login} / '
+ if self.author_name:
+ user_info += f'name: {self.author_name} / '
+
+ return re.sub(r'/ $', '', user_info)
+
+ def get_display_text(self, tag_user: bool = False) -> str:
+
+ if not self.author_id:
+ return f'{self.author_email} is not linked to this commit.\n'
+
+ if not self.is_valid_user():
+ return 'Invalid author details.\n'
+
+ if self.authorized and self.affiliated:
+ return self.get_user_info(tag_user) + ' is authorized.\n'
+
+ if self.affiliated:
+ return self.get_user_info(tag_user) + ' is associated with a company, but not on an approval list.\n'
+ else:
+ return self.get_user_info(tag_user) + ' is not associated with a company.\n'
diff --git a/cla-backend/cla/user_service.py b/cla-backend/cla/user_service.py
index 6c39e1a48..6cbc0397f 100644
--- a/cla-backend/cla/user_service.py
+++ b/cla-backend/cla/user_service.py
@@ -6,7 +6,6 @@
from typing import List
from urllib.parse import quote
-
import requests
import cla
@@ -33,7 +32,7 @@ def get_user_by_sf_id(self, sf_user_id: str):
Queries the platform user service for the specified user id. The
result will return all the details for the user as a dictionary.
"""
- fn = 'user_service.get_user_by_sf_id'
+ fn = 'cla.user_service.get_user_by_sf_id'
headers = {
'Authorization': f'bearer {self.get_access_token()}',
@@ -52,20 +51,20 @@ def get_user_by_sf_id(self, sf_user_id: str):
log.warning(msg)
return None
- def _get_users_by_key_value(self, key: str, value: str):
+ def _get_users_by_key_value(self, key: str, value: str) -> List[dict]:
"""
Queries the platform user service for the specified criteria.
The result will return summary information for the users as a
dictionary.
"""
- fn = 'user_service._get_users_by_key_value'
+ fn = 'cla.user_service._get_users_by_key_value'
headers = {
'Authorization': f'bearer {self.get_access_token()}',
'accept': 'application/json'
}
- users = []
+ users: List[dict] = []
offset = 0
pagesize = 1000
@@ -91,18 +90,18 @@ def _get_users_by_key_value(self, key: str, value: str):
log.debug(f'{fn} - total users : {len(users)}')
return users
- def get_users_by_username(self, user_name: str):
+ def get_users_by_username(self, user_name: str) -> List[dict]:
return self._get_users_by_key_value("username", user_name)
- def get_users_by_firstname(self, first_name: str):
+ def get_users_by_firstname(self, first_name: str) -> List[dict]:
return self._get_users_by_key_value("firstname", first_name)
- def get_users_by_lastname(self, last_name: str):
+ def get_users_by_lastname(self, last_name: str) -> List[dict]:
return self._get_users_by_key_value("lastname", last_name)
- def get_users_by_email(self, email: str):
+ def get_users_by_email(self, email: str) -> List[dict]:
return self._get_users_by_key_value("email", email)
-
+
def has_role(self, username: str, role: str, organization_id: str, cla_group_id: str) -> bool:
"""
Function that checks whether lf user has a role
@@ -117,7 +116,7 @@ def has_role(self, username: str, role: str, organization_id: str, cla_group_id:
:rtype: bool
"""
scopes = {}
- function = 'has_role'
+ function = 'cla.user_service.has_role'
scopes = self._list_org_user_scopes(organization_id, role)
if scopes:
log.info(f'{function} - Found scopes : {scopes} for organization: {organization_id}')
@@ -144,7 +143,7 @@ def has_role(self, username: str, role: str, organization_id: str, cla_group_id:
log.info(f'{function} - {username} does not have role scope')
return False
-
+
def _has_project_org_scope(self, project_sfid: str, organization_id: str, username: str, scopes: dict) -> bool:
"""
Helper function that checks whether there exists project_org_scope for given role
@@ -158,23 +157,26 @@ def _has_project_org_scope(self, project_sfid: str, organization_id: str, userna
:type scopes: dict
:rtype: bool
"""
- function = '_has_project_org_scope_role'
+ function = 'cla.user_service._has_project_org_scope_role'
try:
user_roles = scopes['userroles']
- log.info(f'{function} - User roles: {user_roles}')
+ log.info(f'{function} - User roles for user: \'{username}\' are: {user_roles}')
except KeyError as err:
- log.warning(f'{function} - error: {err} ')
+ log.info(f'{function} - user: \'{username}\' scope does not have \'userroles\', error: {err} '
+ f'Returning False.')
return False
+
+ # For each user role assigned to the user...
for user_role in user_roles:
+ # If the username matches...
if user_role['Contact']['Username'] == username:
- #Since already filtered by role ...get first item
+ # Since already filtered by role ...get first item
for scope in user_role['RoleScopes'][0]['Scopes']:
log.info(f'{function}- Checking objectID for scope: {project_sfid}|{organization_id}')
if scope['ObjectID'] == f'{project_sfid}|{organization_id}':
return True
return False
-
def _list_org_user_scopes(self, organization_id: str, role: str) -> dict:
"""
Helper function that lists the org_user_scopes for a given organization related to given role
@@ -182,12 +184,10 @@ def _list_org_user_scopes(self, organization_id: str, role: str) -> dict:
:type organization_id: string
:param role: role to filter the user org scopes
:type role: string
- :param cla_group_id: cla_group_id thats mapped to salesforce projects
- :type cla_group_id: string
:return: json dict representing org user role scopes
:rtype: dict
"""
- function = '_list_org_user_scopes'
+ function = 'cla.user_service._list_org_user_scopes'
headers = {
'Authorization': f'bearer {self.get_access_token()}',
'accept': 'application/json'
@@ -199,11 +199,12 @@ def _list_org_user_scopes(self, organization_id: str, role: str) -> dict:
r = requests.get(url, headers=headers, params=params)
return r.json()
except requests.exceptions.HTTPError as err:
- log.warning('%s - Could not get user org scopes for organization: %s with role: %s , error: %s ', function, organization_id, role, err)
+ log.warning('%s - Could not get user org scopes for organization: %s with role: %s , error: %s ', function,
+ organization_id, role, err)
return None
def get_access_token(self):
- fn = 'user_service.get_access_token'
+ fn = 'cla.user_service.get_access_token'
# Use previously cached value, if not expired
if self.access_token and datetime.datetime.now() < self.access_token_expires:
cla.log.debug(f'{fn} - using cached access token: {self.access_token[0:10]}...')
diff --git a/cla-backend/cla/utils.py b/cla-backend/cla/utils.py
index fa2b71628..b0f630ab1 100644
--- a/cla-backend/cla/utils.py
+++ b/cla-backend/cla/utils.py
@@ -8,25 +8,32 @@
import inspect
import json
import os
+import re
import urllib.parse
+import urllib.parse as urlparse
from datetime import datetime
from typing import List, Optional
+from urllib.parse import urlencode
+import cla
import falcon
import requests
-from hug.middleware import SessionMiddleware
-from requests_oauthlib import OAuth2Session
-from furl import furl
-
-import cla
+from cla.middleware import CLALogMiddleware
from cla.models import DoesNotExist
-from cla.models.dynamo_models import User, Signature, Repository, \
- Company, Project, Document, \
- GitHubOrg, Gerrit, UserPermissions, Event, CompanyInvite, ProjectCLAGroup, CCLAWhitelistRequest, CLAManagerRequest
+from cla.models.dynamo_models import (CCLAWhitelistRequest, CLAManagerRequest,
+ Company, CompanyInvite, Document, Event,
+ Gerrit, GitHubOrg, GitlabOrg, Project,
+ ProjectCLAGroup, Repository, Signature,
+ User, UserPermissions)
from cla.models.event_types import EventType
+from cla.user import UserCommitSummary
+from hug.middleware import SessionMiddleware
+from requests_oauthlib import OAuth2Session
-API_BASE_URL = os.environ.get('CLA_API_BASE', '')
-CLA_LOGO_URL = os.environ.get('CLA_BUCKET_LOGO_URL', '')
+API_BASE_URL = os.environ.get("CLA_API_BASE", "")
+CLA_LOGO_URL = os.environ.get("CLA_BUCKET_LOGO_URL", "")
+CORPORATE_BASE = os.environ.get("CLA_CORPORATE_BASE", "")
+CORPORATE_V2_BASE = os.environ.get("CLA_CORPORATE_V2_BASE", "")
def get_cla_path():
@@ -36,12 +43,23 @@ def get_cla_path():
return cla_root_dir
+def get_log_middleware():
+ """Prepare the hug middleware to manage logging."""
+ return CLALogMiddleware(logger=cla.log)
+
+
def get_session_middleware():
"""Prepares the hug middleware to manage key-value session data."""
store = get_key_value_store_service()
- return SessionMiddleware(store, context_name='session', cookie_name='cla-sid',
- cookie_max_age=300, cookie_domain=None, cookie_path='/',
- cookie_secure=False)
+ return SessionMiddleware(
+ store,
+ context_name="session",
+ cookie_name="cla-sid",
+ cookie_max_age=300,
+ cookie_domain=None,
+ cookie_path="/",
+ cookie_secure=False,
+ )
def create_database(conf=None):
@@ -54,11 +72,11 @@ def create_database(conf=None):
"""
if conf is None:
conf = cla.conf
- cla.log.info('Creating CLA database in %s', conf['DATABASE'])
- if conf['DATABASE'] == 'DynamoDB':
+ cla.log.info("Creating CLA database in %s", conf["DATABASE"])
+ if conf["DATABASE"] == "DynamoDB":
from cla.models.dynamo_models import create_database as cd
else:
- raise Exception('Invalid database selection in configuration: %s' % conf['DATABASE'])
+ raise Exception("Invalid database selection in configuration: %s" % conf["DATABASE"])
cd()
@@ -74,11 +92,11 @@ def delete_database(conf=None):
"""
if conf is None:
conf = cla.conf
- cla.log.warning('Deleting CLA database in %s', conf['DATABASE'])
- if conf['DATABASE'] == 'DynamoDB':
+ cla.log.warning("Deleting CLA database in %s", conf["DATABASE"])
+ if conf["DATABASE"] == "DynamoDB":
from cla.models.dynamo_models import delete_database as dd
else:
- raise Exception('Invalid database selection in configuration: %s' % conf['DATABASE'])
+ raise Exception("Invalid database selection in configuration: %s" % conf["DATABASE"])
dd()
@@ -98,15 +116,25 @@ def get_database_models(conf=None):
"""
if conf is None:
conf = cla.conf
- if conf['DATABASE'] == 'DynamoDB':
- return {'User': User, 'Signature': Signature, 'Repository': Repository,
- 'Company': Company, 'Project': Project, 'Document': Document,
- 'GitHubOrg': GitHubOrg, 'Gerrit': Gerrit, 'UserPermissions': UserPermissions,
- 'Event': Event, 'CompanyInvites': CompanyInvite, 'ProjectCLAGroup': ProjectCLAGroup,
- 'CCLAWhitelistRequest': CCLAWhitelistRequest, 'CLAManagerRequest': CLAManagerRequest,
- }
+ if conf["DATABASE"] == "DynamoDB":
+ return {
+ "User": User,
+ "Signature": Signature,
+ "Repository": Repository,
+ "Company": Company,
+ "Project": Project,
+ "Document": Document,
+ "GitHubOrg": GitHubOrg,
+ "Gerrit": Gerrit,
+ "UserPermissions": UserPermissions,
+ "Event": Event,
+ "CompanyInvites": CompanyInvite,
+ "ProjectCLAGroup": ProjectCLAGroup,
+ "CCLAWhitelistRequest": CCLAWhitelistRequest,
+ "CLAManagerRequest": CLAManagerRequest,
+ }
else:
- raise Exception('Invalid database selection in configuration: %s' % conf['DATABASE'])
+ raise Exception("Invalid database selection in configuration: %s" % conf["DATABASE"])
def get_user_instance(conf=None) -> User:
@@ -118,7 +146,7 @@ def get_user_instance(conf=None) -> User:
:return: A User model instance based on configuration specified.
:rtype: cla.models.model_interfaces.User
"""
- return get_database_models(conf)['User']()
+ return get_database_models(conf)["User"]()
def get_cla_manager_requests_instance(conf=None) -> CLAManagerRequest:
@@ -130,7 +158,7 @@ def get_cla_manager_requests_instance(conf=None) -> CLAManagerRequest:
:return: A CLAManagerRequest model instance based on configuration specified.
:rtype: cla.models.model_interfaces.CLAManagerRequest
"""
- return get_database_models(conf)['CLAManagerRequest']()
+ return get_database_models(conf)["CLAManagerRequest"]()
def get_user_permissions_instance(conf=None) -> UserPermissions:
@@ -142,7 +170,7 @@ def get_user_permissions_instance(conf=None) -> UserPermissions:
:return: A UserPermissions model instance based on configuration specified
:rtype: cla.models.model_interfaces.UserPermissions
"""
- return get_database_models(conf)['UserPermissions']()
+ return get_database_models(conf)["UserPermissions"]()
def get_company_invites_instance(conf=None):
@@ -154,7 +182,7 @@ def get_company_invites_instance(conf=None):
:return: A CompanyInvites model instance based on configuration specified
:rtype: cla.models.model_interfaces.CompanyInvite
"""
- return get_database_models(conf)['CompanyInvites']()
+ return get_database_models(conf)["CompanyInvites"]()
def get_signature_instance(conf=None) -> Signature:
@@ -166,7 +194,7 @@ def get_signature_instance(conf=None) -> Signature:
:return: An Signature model instance based on configuration.
:rtype: cla.models.model_interfaces.Signature
"""
- return get_database_models(conf)['Signature']()
+ return get_database_models(conf)["Signature"]()
def get_repository_instance(conf=None):
@@ -178,7 +206,7 @@ def get_repository_instance(conf=None):
:return: A Repository model instance based on configuration specified.
:rtype: cla.models.model_interfaces.Repository
"""
- return get_database_models(conf)['Repository']()
+ return get_database_models(conf)["Repository"]()
def get_github_organization_instance(conf=None):
@@ -190,7 +218,7 @@ def get_github_organization_instance(conf=None):
:return: A Repository model instance based on configuration specified.
:rtype: cla.models.model_interfaces.GitHubOrg
"""
- return get_database_models(conf)['GitHubOrg']()
+ return get_database_models(conf)["GitHubOrg"]()
def get_gerrit_instance(conf=None):
@@ -202,7 +230,7 @@ def get_gerrit_instance(conf=None):
:return: A Gerrit model instance based on configuration specified.
:rtype: cla.models.model_interfaces.Gerrit
"""
- return get_database_models(conf)['Gerrit']()
+ return get_database_models(conf)["Gerrit"]()
def get_company_instance(conf=None) -> Company:
@@ -214,7 +242,7 @@ def get_company_instance(conf=None) -> Company:
:return: A company model instance based on configuration specified.
:rtype: cla.models.model_interfaces.Company
"""
- return get_database_models(conf)['Company']()
+ return get_database_models(conf)["Company"]()
def get_project_instance(conf=None) -> Project:
@@ -226,7 +254,7 @@ def get_project_instance(conf=None) -> Project:
:return: A Project model instance based on configuration specified.
:rtype: cla.models.model_interfaces.Project
"""
- return get_database_models(conf)['Project']()
+ return get_database_models(conf)["Project"]()
def get_document_instance(conf=None):
@@ -238,7 +266,7 @@ def get_document_instance(conf=None):
:return: A Document model instance based on configuration specified.
:rtype: cla.models.model_interfaces.Document
"""
- return get_database_models(conf)['Document']()
+ return get_database_models(conf)["Document"]()
def get_event_instance(conf=None) -> Event:
@@ -250,7 +278,7 @@ def get_event_instance(conf=None) -> Event:
:return: A Event model instance based on configuration
:rtype: cla.models.model_interfaces.Event
"""
- return get_database_models(conf)['Event']()
+ return get_database_models(conf)["Event"]()
def get_project_cla_group_instance(conf=None) -> ProjectCLAGroup:
@@ -263,7 +291,7 @@ def get_project_cla_group_instance(conf=None) -> ProjectCLAGroup:
:rtype: cla.models.model_interfaces.ProjectCLAGroup
"""
- return get_database_models(conf)['ProjectCLAGroup']()
+ return get_database_models(conf)["ProjectCLAGroup"]()
def get_ccla_whitelist_request_instance(conf=None) -> CCLAWhitelistRequest:
@@ -276,7 +304,7 @@ def get_ccla_whitelist_request_instance(conf=None) -> CCLAWhitelistRequest:
:rtype: cla.models.model_interfaces.CCLAWhitelistRequest
"""
- return get_database_models(conf)['CCLAWhitelistRequest']()
+ return get_database_models(conf)["CCLAWhitelistRequest"]()
def get_email_service(conf=None, initialize=True):
@@ -292,19 +320,19 @@ def get_email_service(conf=None, initialize=True):
"""
if conf is None:
conf = cla.conf
- email_service = conf['EMAIL_SERVICE']
- if email_service == 'SMTP':
+ email_service = conf["EMAIL_SERVICE"]
+ if email_service == "SMTP":
from cla.models.smtp_models import SMTP as email
- elif email_service == 'MockSMTP':
+ elif email_service == "MockSMTP":
from cla.models.smtp_models import MockSMTP as email
- elif email_service == 'SES':
+ elif email_service == "SES":
from cla.models.ses_models import SES as email
- elif email_service == 'SNS':
+ elif email_service == "SNS":
from cla.models.sns_email_models import SNS as email
- elif email_service == 'MockSES':
+ elif email_service == "MockSES":
from cla.models.ses_models import MockSES as email
else:
- raise Exception('Invalid email service selected in configuration: %s' % email_service)
+ raise Exception("Invalid email service selected in configuration: %s" % email_service)
email_instance = email()
if initialize:
email_instance.initialize(conf)
@@ -324,13 +352,13 @@ def get_signing_service(conf=None, initialize=True):
"""
if conf is None:
conf = cla.conf
- signing_service = conf['SIGNING_SERVICE']
- if signing_service == 'DocuSign':
+ signing_service = conf["SIGNING_SERVICE"]
+ if signing_service == "DocuSign":
from cla.models.docusign_models import DocuSign as signing
- elif signing_service == 'MockDocuSign':
+ elif signing_service == "MockDocuSign":
from cla.models.docusign_models import MockDocuSign as signing
else:
- raise Exception('Invalid signing service selected in configuration: %s' % signing_service)
+ raise Exception("Invalid signing service selected in configuration: %s" % signing_service)
signing_service_instance = signing()
if initialize:
signing_service_instance.initialize(conf)
@@ -350,15 +378,15 @@ def get_storage_service(conf=None, initialize=True):
"""
if conf is None:
conf = cla.conf
- storage_service = conf['STORAGE_SERVICE']
- if storage_service == 'LocalStorage':
+ storage_service = conf["STORAGE_SERVICE"]
+ if storage_service == "LocalStorage":
from cla.models.local_storage import LocalStorage as storage
- elif storage_service == 'S3Storage':
+ elif storage_service == "S3Storage":
from cla.models.s3_storage import S3Storage as storage
- elif storage_service == 'MockS3Storage':
+ elif storage_service == "MockS3Storage":
from cla.models.s3_storage import MockS3Storage as storage
else:
- raise Exception('Invalid storage service selected in configuration: %s' % storage_service)
+ raise Exception("Invalid storage service selected in configuration: %s" % storage_service)
storage_instance = storage()
if initialize:
storage_instance.initialize(conf)
@@ -378,13 +406,13 @@ def get_pdf_service(conf=None, initialize=True):
"""
if conf is None:
conf = cla.conf
- pdf_service = conf['PDF_SERVICE']
- if pdf_service == 'DocRaptor':
+ pdf_service = conf["PDF_SERVICE"]
+ if pdf_service == "DocRaptor":
from cla.models.docraptor_models import DocRaptor as pdf
- elif pdf_service == 'MockDocRaptor':
+ elif pdf_service == "MockDocRaptor":
from cla.models.docraptor_models import MockDocRaptor as pdf
else:
- raise Exception('Invalid PDF service selected in configuration: %s' % pdf_service)
+ raise Exception("Invalid PDF service selected in configuration: %s" % pdf_service)
pdf_instance = pdf()
if initialize:
pdf_instance.initialize(conf)
@@ -402,13 +430,13 @@ def get_key_value_store_service(conf=None):
"""
if conf is None:
conf = cla.conf
- keyvalue = cla.conf['KEYVALUE']
- if keyvalue == 'Memory':
+ keyvalue = cla.conf["KEYVALUE"]
+ if keyvalue == "Memory":
from hug.store import InMemoryStore as Store
- elif keyvalue == 'DynamoDB':
+ elif keyvalue == "DynamoDB":
from cla.models.dynamo_models import Store
else:
- raise Exception('Invalid key-value store selected in configuration: %s' % keyvalue)
+ raise Exception("Invalid key-value store selected in configuration: %s" % keyvalue)
return Store()
@@ -421,10 +449,11 @@ def get_supported_repository_providers():
:rtype: dict
"""
from cla.models.github_models import GitHub, MockGitHub
+
# from cla.models.gitlab_models import GitLab, MockGitLab
# return {'github': GitHub, 'mock_github': MockGitHub,
# 'gitlab': GitLab, 'mock_gitlab': MockGitLab}
- return {'github': GitHub, 'mock_github': MockGitHub}
+ return {"github": GitHub, "mock_github": MockGitHub}
def get_repository_service(provider, initialize=True):
@@ -440,7 +469,7 @@ def get_repository_service(provider, initialize=True):
"""
providers = get_supported_repository_providers()
if provider not in providers:
- raise NotImplementedError('Provider not supported')
+ raise NotImplementedError("Provider not supported")
instance = providers[provider]()
if initialize:
instance.initialize(cla.conf)
@@ -459,7 +488,7 @@ def get_repository_service_by_repository(repository, initialize=True):
:return: A repository provider instance (GitHub, Gerrit, etc).
:rtype: RepositoryService
"""
- repository_model = get_database_models()['Repository']
+ repository_model = get_database_models()["Repository"]
if isinstance(repository, repository_model):
repo = repository
else:
@@ -476,7 +505,7 @@ def get_supported_document_content_types(): # pylint: disable=invalid-name
:return: List of supported document content types.
:rtype: dict
"""
- return ['pdf', 'url+pdf', 'storage+pdf']
+ return ["pdf", "url+pdf", "storage+pdf"]
def get_project_document(project, document_type, major_version, minor_version):
@@ -494,13 +523,12 @@ def get_project_document(project, document_type, major_version, minor_version):
:return: The document model if found.
:rtype: cla.models.model_interfaces.Document
"""
- if document_type == 'individual':
+ if document_type == "individual":
documents = project.get_project_individual_documents()
else:
documents = project.get_project_corporate_documents()
for document in documents:
- if document.get_document_major_version() == major_version and \
- document.get_document_minor_version() == minor_version:
+ if document.get_document_major_version() == major_version and document.get_document_minor_version() == minor_version:
return document
return None
@@ -562,48 +590,62 @@ def get_last_version(documents):
def user_icla_check(user: User, project: Project, signature: Signature, latest_major_version=False) -> bool:
- cla.log.debug(f'ICLA signature found for user: {user} on project: {project}, '
- f'signature_id: {signature.get_signature_id()}')
+ cla.log.debug(
+ f"ICLA signature found for user: {user} on project: {project}, " f"signature_id: {signature.get_signature_id()}"
+ )
# Here's our logic to determine if the signature is valid
if latest_major_version: # Ensure it's latest signature.
document_models = project.get_project_individual_documents()
major, _ = get_last_version(document_models)
if signature.get_signature_document_major_version() != major:
- cla.log.debug(f'User: {user} only has an old document version signed '
- f'(v{signature.get_signature_document_major_version()}) - needs a new version')
+ cla.log.debug(
+ f"User: {user} only has an old document version signed "
+ f"(v{signature.get_signature_document_major_version()}) - needs a new version"
+ )
return False
if signature.get_signature_signed() and signature.get_signature_approved():
# Signature found and signed/approved.
- cla.log.debug(f'User: {user} has ICLA signed and approved signature_id: {signature.get_signature_id()} '
- f'for project: {project}')
+ cla.log.debug(
+ f"User: {user} has ICLA signed and approved signature_id: {signature.get_signature_id()} "
+ f"for project: {project}"
+ )
return True
elif signature.get_signature_signed(): # Not approved yet.
- cla.log.debug(f'User: {user} has ICLA signed with signature_id: {signature.get_signature_id()}, '
- f'project: {project}, but has not been approved yet')
+ cla.log.debug(
+ f"User: {user} has ICLA signed with signature_id: {signature.get_signature_id()}, "
+ f"project: {project}, but has not been approved yet"
+ )
return False
else: # Not signed or approved yet.
- cla.log.debug(f'User: {user} has ICLA with signature_id: {signature.get_signature_id()}, '
- f'project: {project}, but has not been signed or approved yet')
+ cla.log.debug(
+ f"User: {user} has ICLA with signature_id: {signature.get_signature_id()}, "
+ f"project: {project}, but has not been signed or approved yet"
+ )
return False
def user_ccla_check(user: User, project: Project, signature: Signature) -> bool:
- cla.log.debug(f'CCLA signature found for user: {user} on project: {project}, '
- f'signature_id: {signature.get_signature_id()}')
+ cla.log.debug(
+ f"CCLA signature found for user: {user} on project: {project}, " f"signature_id: {signature.get_signature_id()}"
+ )
if signature.get_signature_signed() and signature.get_signature_approved():
- cla.log.debug(f'User: {user} has a signed and approved CCLA for project: {project}')
+ cla.log.debug(f"User: {user} has a signed and approved CCLA for project: {project}")
return True
if signature.get_signature_signed():
- cla.log.debug(f'User: {user} has CCLA signed with signature_id: {signature.get_signature_id()}, '
- f'project: {project}, but has not been approved yet')
+ cla.log.debug(
+ f"User: {user} has CCLA signed with signature_id: {signature.get_signature_id()}, "
+ f"project: {project}, but has not been approved yet"
+ )
return False
else: # Not signed or approved yet.
- cla.log.debug(f'User: {user} has CCLA with signature_id: {signature.get_signature_id()}, '
- f'project: {project}, but has not been signed or approved yet')
+ cla.log.debug(
+ f"User: {user} has CCLA with signature_id: {signature.get_signature_id()}, "
+ f"project: {project}, but has not been signed or approved yet"
+ )
return False
@@ -621,88 +663,112 @@ def user_signed_project_signature(user: User, project: Project) -> bool:
:rtype: boolean
"""
- fn = 'utils.user_signed_project_signature'
+ fn = "utils.user_signed_project_signature"
# Check if we have an ICLA for this user
- cla.log.debug(f'{fn} - checking to see if user has signed an ICLA, user: {user}, project: {project}')
+ cla.log.debug(f"{fn} - checking to see if user has signed an ICLA, user: {user}, project: {project}")
signature = user.get_latest_signature(project.get_project_id(), signature_signed=True, signature_approved=True)
icla_pass = False
if signature is not None:
icla_pass = True
else:
- cla.log.debug(f'{fn} - ICLA signature NOT found for User: {user} on project: {project}')
+ cla.log.debug(f"{fn} - ICLA signature NOT found for User: {user} on project: {project}")
# If we passed the ICLA check - good, return true, no need to check CCLA
if icla_pass:
- cla.log.debug(
- f'{fn} - ICLA signature check passed for User: {user} on project: {project} - skipping CCLA check')
+ cla.log.debug(f"{fn} - ICLA signature check passed for User: {user} on project: {project} - skipping CCLA check")
return True
else:
- cla.log.debug(
- f'{fn} - ICLA signature check failed for User: {user} on project: {project} - will now check CCLA')
+ cla.log.debug(f"{fn} - ICLA signature check failed for User: {user} on project: {project} - will now check CCLA")
# Check if we have an CCLA for this user
company_id = user.get_user_company_id()
ccla_pass = False
if company_id is not None:
- cla.log.debug(f'{fn} - CCLA signature check - user has a company: {company_id} - '
- 'looking up user\'s employee acknowledgement...')
+ cla.log.debug(
+ f"{fn} - CCLA signature check - user has a company: {company_id} - "
+ "looking up user's employee acknowledgement..."
+ )
# Get employee signature
employee_signature = user.get_latest_signature(
- project.get_project_id(),
- company_id=company_id,
- signature_signed=True,
- signature_approved=True)
+ project.get_project_id(), company_id=company_id, signature_signed=True, signature_approved=True
+ )
if employee_signature is not None:
- cla.log.debug(f'{fn} - CCLA signature check - located employee acknowledgement - '
- f'signature id: {employee_signature.get_signature_id()}')
+ cla.log.debug(
+ f"{fn} - CCLA signature check - located employee acknowledgement - "
+ f"signature id: {employee_signature.get_signature_id()}"
+ )
- cla.log.debug(f'{fn} - CCLA signature check - loading company record by id: {company_id}...')
company = get_company_instance()
- company.load(company_id)
+ try:
+ cla.log.debug(f"{fn} - CCLA signature check - loading company record by id: {company_id}...")
+ company.load(company_id)
+ except DoesNotExist as err:
+ cla.log.debug(
+ f"{fn} - CCLA signature check failed - user is NOT associated with a valid company - "
+ f"company with id does not exist: {company_id}."
+ )
+ return False
# Get CCLA signature of company to access whitelist
- cla.log.debug(f'{fn} - CCLA signature check - loading signed CCLA for project|company, '
- f'user: {user}, project_id: {project}, company_id: {company_id}')
+ cla.log.debug(
+ f"{fn} - CCLA signature check - loading signed CCLA for project|company, "
+ f"user: {user}, project_id: {project}, company_id: {company_id}"
+ )
signature = company.get_latest_signature(
- project.get_project_id(), signature_signed=True, signature_approved=True)
+ project.get_project_id(), signature_signed=True, signature_approved=True
+ )
# Don't check the version for employee signatures.
if signature is not None:
- cla.log.debug(f'{fn} - CCLA signature check - loaded signed CCLA for project|company, '
- f'user: {user}, project_id: {project}, company_id: {company_id}, '
- f'signature_id: {signature.get_signature_id()}')
+ cla.log.debug(
+ f"{fn} - CCLA signature check - loaded signed CCLA for project|company, "
+ f"user: {user}, project_id: {project}, company_id: {company_id}, "
+ f"signature_id: {signature.get_signature_id()}"
+ )
# Verify if user has been approved: https://github.com/communitybridge/easycla/issues/332
- cla.log.debug(f'{fn} - CCLA signature check - '
- 'checking to see if the user is in one of the approval lists...')
+ cla.log.debug(
+ f"{fn} - CCLA signature check - " "checking to see if the user is in one of the approval lists..."
+ )
+ # if project.get_project_ccla_requires_icla_signature() is True:
+ # cla.log.debug(f'{fn} - CCLA signature check - '
+ # 'project requires ICLA signature as well as CCLA signature ')
if user.is_approved(signature):
ccla_pass = True
else:
# Set user signatures approved = false due to user failing whitelist checks
- cla.log.debug(f'{fn} - user not in one of the approval lists - '
- 'marking signature approved = false for '
- f'user: {user}, project_id: {project}, company_id: {company_id}')
+ cla.log.debug(
+ f"{fn} - user not in one of the approval lists - "
+ "marking signature approved = false for "
+ f"user: {user}, project_id: {project}, company_id: {company_id}"
+ )
user_signatures = user.get_user_signatures(
- project_id=project.get_project_id(), company_id=company_id, signature_approved=True,
- signature_signed=True
+ project_id=project.get_project_id(),
+ company_id=company_id,
+ signature_approved=True,
+ signature_signed=True,
)
for signature in user_signatures:
- cla.log.debug(f'{fn} - user not in one of the approval lists - '
- 'marking signature approved = false for '
- f'user: {user}, project_id: {project}, company_id: {company_id}, '
- f'signature: {signature.get_signature_id()}')
+ cla.log.debug(
+ f"{fn} - user not in one of the approval lists - "
+ "marking signature approved = false for "
+ f"user: {user}, project_id: {project}, company_id: {company_id}, "
+ f"signature: {signature.get_signature_id()}"
+ )
signature.set_signature_approved(False)
signature.save()
- event_data = (f'The employee signature of user {user.get_user_name()} was '
- f'disapproved the during CCLA check for project {project.get_project_name()} '
- f'and company {company.get_company_name()}')
+ event_data = (
+ f"The employee signature of user {user.get_user_name()} was "
+ f"disapproved the during CCLA check for project {project.get_project_name()} "
+ f"and company {company.get_company_name()}"
+ )
Event.create_event(
event_type=EventType.EmployeeSignatureDisapproved,
- event_project_id=project.get_project_id(),
+ event_cla_group_id=project.get_project_id(),
event_company_id=company.get_company_id(),
event_user_id=user.get_user_id(),
event_data=event_data,
@@ -710,25 +776,31 @@ def user_signed_project_signature(user: User, project: Project) -> bool:
contains_pii=True,
)
else:
- cla.log.debug(f'{fn} - CCLA signature check - unable to load signed CCLA for project|company, '
- f'user: {user}, project_id: {project}, company_id: {company_id} - '
- 'signatory needs to sign the CCLA before the user can be authorized')
+ cla.log.debug(
+ f"{fn} - CCLA signature check - unable to load signed CCLA for project|company, "
+ f"user: {user}, project_id: {project}, company_id: {company_id} - "
+ "signatory needs to sign the CCLA before the user can be authorized"
+ )
else:
- cla.log.debug(f'{fn} - CCLA signature check - unable to load employee acknowledgement for project|company, '
- f'user: {user}, project_id: {project}, company_id: {company_id}, '
- 'signed=true, approved=true - user needs to be associated with an organization before '
- 'they can be authorized.')
+ cla.log.debug(
+ f"{fn} - CCLA signature check - unable to load employee acknowledgement for project|company, "
+ f"user: {user}, project_id: {project}, company_id: {company_id}, "
+ "signed=true, approved=true - user needs to be associated with an organization before "
+ "they can be authorized."
+ )
else:
- cla.log.debug(f'{fn} - CCLA signature check failed - user is NOT associated with a company - '
- f'unable to check for a CCLA, user info: {user}.')
+ cla.log.debug(
+ f"{fn} - CCLA signature check failed - user is NOT associated with a company - "
+ f"unable to check for a CCLA, user info: {user}."
+ )
if ccla_pass:
- cla.log.debug(f'{fn} - CCLA signature check passed for user: {user} on project: {project}')
+ cla.log.debug(f"{fn} - CCLA signature check passed for user: {user} on project: {project}")
return True
else:
- cla.log.debug(f'{fn} - CCLA signature check failed for user: {user} on project: {project}')
+ cla.log.debug(f"{fn} - CCLA signature check failed for user: {user} on project: {project}")
- cla.log.debug(f'{fn} - User: {user} failed both ICLA and CCLA checks')
+ cla.log.debug(f"{fn} - User: {user} failed both ICLA and CCLA checks")
return False
@@ -749,12 +821,13 @@ def get_redirect_uri(repository_service, installation_id, github_repository_id,
:return: The redirect_uri parameter expected by the OAuth2 process.
:rtype: string
"""
- params = {'installation_id': installation_id,
- 'github_repository_id': github_repository_id,
- 'change_request_id': change_request_id}
+ params = {
+ "installation_id": installation_id,
+ "github_repository_id": github_repository_id,
+ "change_request_id": change_request_id,
+ }
params = urllib.parse.urlencode(params)
- return '{}/v2/repository-provider/{}/oauth2_redirect?{}'.format(cla.conf['API_BASE_URL'], repository_service,
- params)
+ return "{}/v2/repository-provider/{}/oauth2_redirect?{}".format(cla.conf["API_BASE_URL"], repository_service, params)
def get_full_sign_url(repository_service, installation_id, github_repository_id, change_request_id, project_version):
@@ -778,10 +851,9 @@ def get_full_sign_url(repository_service, installation_id, github_repository_id,
:type project_version: string
"""
- base_url = '{}/v2/repository-provider/{}/sign/{}/{}/{}'.format(cla.conf['API_BASE_URL'], repository_service,
- str(installation_id),
- str(github_repository_id),
- str(change_request_id))
+ base_url = "{}/v2/repository-provider/{}/sign/{}/{}/{}/#/".format(
+ cla.conf["API_BASE_URL"], repository_service, str(installation_id), str(github_repository_id), str(change_request_id)
+ )
return append_project_version_to_url(address=base_url, project_version=project_version)
@@ -794,18 +866,32 @@ def append_project_version_to_url(address: str, project_version: str) -> str:
:return: returns the final url
"""
version = "1"
- if project_version and project_version == 'v2':
+ if project_version and project_version == "v2":
version = "2"
- f = furl(address)
- if "version" in f.args:
+ # seem if the url has # in it (https://dev.lfcla.com/#/version=1) the underlying urllib is being confused
+ # In[21]: list(urlparse.urlparse(address))
+ # Out[21]: ['https', 'dev.lfcla.com', '/', '', '', '/#/?version=1']
+
+ query = {}
+ if "?" in address:
+ query = dict(urlparse.parse_qsl(address.split("?")[1]))
+
+ # we don't alter for now
+ if "version" in query:
return address
- f.args["version"] = version
- return f.url
+ query["version"] = version
+ query_params_str = urlencode(query)
+
+ if "?" in address:
+ return "?".join([address.split("?")[0], query_params_str])
+ return "?".join([address, query_params_str])
-def get_comment_badge(repository_type, all_signed, sign_url, project_version, missing_user_id=False,
- is_approved_by_manager=False):
+
+def get_comment_badge(
+ repository_type, all_signed, sign_url, project_version, missing_user_id=False, is_approved_by_manager=False
+):
"""
Returns the CLA badge that will appear on the change request comment (PR for 'github', merge
request for 'gitlab', etc)
@@ -823,27 +909,48 @@ def get_comment_badge(repository_type, all_signed, sign_url, project_version, mi
:type is_approved_by_manager: bool
"""
- alt = 'CLA'
+ alt = "CLA"
if all_signed:
- badge_url = f'{CLA_LOGO_URL}/cla-signed.svg'
+ badge_url = f"{CLA_LOGO_URL}/cla-signed.svg"
badge_hyperlink = cla.conf["CLA_LANDING_PAGE"]
+ badge_hyperlink = os.path.join(badge_hyperlink, "#/")
badge_hyperlink = append_project_version_to_url(address=badge_hyperlink, project_version=project_version)
alt = "CLA Signed"
+ return (
+ f''
+ f''
+ "
"
+ )
else:
+ badge_hyperlink = sign_url
+ text = ""
if missing_user_id:
- badge_url = f'{CLA_LOGO_URL}/cla-missing-id.svg'
- alt = 'CLA Missing ID'
- elif is_approved_by_manager:
- badge_url = f'{CLA_LOGO_URL}/cla-confirmation-needed.svg'
- alt = 'CLA Confirmation Needed'
+ badge_url = f"{CLA_LOGO_URL}/cla-missing-id.svg"
+ alt = "CLA Missing ID"
+ text = (
+ f'{text} '
+ f''
+ ""
+ )
+
+ if is_approved_by_manager:
+ badge_url = f"{CLA_LOGO_URL}/cla-confirmation-needed.svg"
+ alt = "CLA Confirmation Needed"
+ text = (
+ f'{text} '
+ f'
'
+ ""
+ )
else:
- badge_url = f'{CLA_LOGO_URL}/cla-not-signed.svg'
+ badge_url = f"{CLA_LOGO_URL}/cla-not-signed.svg"
alt = "CLA Not Signed"
- badge_hyperlink = sign_url
- # return '[](' + badge_hyperlink + ')'
- return (f''
- f'
'
- '
')
+ text = (
+ f'{text} '
+ f''
+ ""
+ )
+
+ return f"{text}
"
def assemble_cla_status(author_name, signed=False):
@@ -859,14 +966,21 @@ def assemble_cla_status(author_name, signed=False):
:type signed: boolean
"""
if author_name is None:
- author_name = 'Unknown'
+ author_name = "Unknown"
if signed:
- return author_name, 'EasyCLA check passed. You are authorized to contribute.'
- return author_name, 'Missing CLA Authorization.'
+ return author_name, "EasyCLA check passed. You are authorized to contribute."
+ return author_name, "Missing CLA Authorization."
-def assemble_cla_comment(repository_type, installation_id, github_repository_id, change_request_id, signed, missing,
- project_version):
+def assemble_cla_comment(
+ repository_type,
+ installation_id,
+ github_repository_id,
+ change_request_id,
+ signed: List[UserCommitSummary],
+ missing: List[UserCommitSummary],
+ project_version,
+):
"""
Helper function to generate a CLA comment based on a a change request.
@@ -882,48 +996,50 @@ def assemble_cla_comment(repository_type, installation_id, github_repository_id,
:type github_repository_id: int
:param change_request_id: The repository service's ID of this change request.
:type change_request_id: id
- :param signed: The list of commit hashes and authors that have signed an signature for this
- change request.
- :type signed: [(string, string)]
- :param missing: The list of commit hashes and authors that have not signed for this
+ :param signed: The list of user commit summary objects indicating which authors that have signed an signature for
+ this change request.
+ :type signed: List[UserCommitSummary]
+ :param missing: The list of user commit summary objects indicating which authors have not signed for this
change request.
- :type missing: [(string, list)]
+ :type missing: List[UserCommitSummary]
:param project_version: Project version associated with PR comment
:type project_version: string
"""
- num_missing = len(missing)
- missing_ids = list(filter(lambda x: x[1][0] is None, missing))
- no_user_id = len(missing_ids) > 0
+
+ # missing_ids = list(filter(lambda x: (x[1] is None or len(x[1]) == 0), missing))
+
+ # Test to see if any of the users in the missing category are missing their user id
+ no_user_id = len(list(filter(lambda x: (x.author_id is None), missing))) > 0
+
# check if an unsigned committer has been approved by a CLA Manager, but not associated with a company
# Logic not supported as we removed the DB query in the caller
# approved_ids = list(filter(lambda x: len(x[1]) == 4 and x[1][3] is True, missing))
# approved_by_manager = len(approved_ids) > 0
- sign_url = get_full_sign_url(repository_type, installation_id, github_repository_id, change_request_id,
- project_version)
+ sign_url = get_full_sign_url(repository_type, installation_id, github_repository_id, change_request_id, project_version)
comment = get_comment_body(repository_type, sign_url, signed, missing)
- all_signed = num_missing == 0
+ all_signed = len(missing) == 0
badge = get_comment_badge(
repository_type=repository_type,
all_signed=all_signed,
sign_url=sign_url,
project_version=project_version,
- missing_user_id=no_user_id)
- return badge + '
' + comment
+ missing_user_id=no_user_id,
+ )
+ return badge + "
" + comment
-def get_comment_body(repository_type, sign_url, signed, missing):
+def get_comment_body(repository_type, sign_url, signed: List[UserCommitSummary], missing: List[UserCommitSummary]):
"""
Returns the CLA comment that will appear on the repository provider's change request item.
- :param repository_type: The repository type where this comment will be posted ('github',
- 'gitlab', etc).
- :type repository_type: string
- :param sign_url: The URL for the user to click in order to initiate signing.
- :type sign_url: string
- :param signed: List of tuples containing the commit and author name of signers.
- :type signed: [(string, string)]
- :param missing: List of tuples containing the commit and author name of not-signed users.
- :type missing: [(string, list)]
+ :param: repository_type: The repository type where this comment will be posted ('github', 'gitlab', etc).
+ :type: repository_type: string
+ :param: sign_url: The URL for the user to click in order to initiate signing.
+ :type: sign_url: string
+ :param: signed: List of user commit summary objects containing the commit and author name of signers.
+ :type: signed: List[UserCommitSummary]
+ :param: missing: List of user commit summary objects containing the commit and author name of not-signed users.
+ :type: missing: List[UserCommitSummary]
"""
fn = "utils.get_comment_body"
cla.log.info(f"{fn} - Getting comment body for repository type: %s", repository_type)
@@ -932,77 +1048,115 @@ def get_comment_body(repository_type, sign_url, signed, missing):
committers_comment = ""
num_signed = len(signed)
num_missing = len(missing)
+ text = ""
+
+ # Start of the HTML to render the list of committers
+ if len(signed) > 0 or len(missing) > 0:
+ committers_comment += "
If you need help or have questions about EasyCLA, you can read the documentation or reach out to us for support.
' def get_email_sign_off_content() -> str: - return 'Thanks,
The LF Engineering Team
' + return "Thanks,
EasyCLA Support Team
" + + +def get_corporate_url(project_version: str) -> str: + """ + helper method that returns appropriate corporate link based on EasyCLA version + :param project_version: cla_group version(v1|v2) + :return: default is v1 corporate console + """ + return CORPORATE_V2_BASE if project_version == "v2" else CORPORATE_BASE def append_email_help_sign_off_content(body: str, project_version: str) -> str: @@ -1536,11 +1856,13 @@ def append_email_help_sign_off_content(body: str, project_version: str) -> str: :param project_version: :return: """ - return "".join([ - body, - get_email_help_content(project_version == "v2"), - get_email_sign_off_content(), - ]) + return "".join( + [ + body, + get_email_help_content(project_version == "v2"), + get_email_sign_off_content(), + ] + ) def append_email_help_sign_off_content_plain(body: str, project_version: str) -> str: @@ -1570,9 +1892,56 @@ def get_formatted_time(the_time: datetime) -> str: return the_time.strftime("%Y-%m-%dT%H:%M:%S.%f%z") + "+0000" +def get_time_from_string(date_string: str) -> Optional[datetime]: + """ + Helper function to return the specified datetime object from an ISO standard format string + :return: + """ + # Try these formats + formats = ["%Y-%m-%d %H:%M:%S.%f%z", "%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%dT%H:%M:%S.%f%z", "%Y-%m-%dT%H:%M:%S.%f"] + for fmt in formats: + try: + return datetime.strptime(date_string, fmt) + except (ValueError, TypeError) as e: + pass + # print(f'unable to parse time {date_string} using {fmt}, error: {e}') + return None + + def get_public_email(user): """ Helper function to return public user email to send emails """ if len(user.get_all_user_emails()) > 0: return next((email for email in user.get_all_user_emails() if "noreply.github.com" not in email), None) + + +def get_co_authors_from_commit(commit): + """ + Helper function to return co-authors from commit + """ + fn = "get_co_authors_from_commit" + co_authors = [] + if commit.commit: + commit_message = commit.commit.message + cla.log.debug(f"{fn} - commit message: {commit_message}") + if commit_message: + co_authors = re.findall(r"Co-authored-by: (.*) <(.*)>", commit_message) + return co_authors + + +def extract_pull_request_number(pull_request_message): + """ + Helper function to return pull request number from pull request message + :param pull_request_message: message in merge_group payload + :return: + """ + fn = "extract_pull_request_number" + pull_request_number = None + try: + pull_request_number = int(re.search(r"#(\d+)", pull_request_message).group(1)) + except AttributeError as e: + cla.log.warning(f"{fn} - unable to extract pull request number from message: {pull_request_message}, error: {e}") + except Exception as e: + cla.log.warning(f"{fn} - unable to extract pull request number from message: {pull_request_message}, error: {e}") + return pull_request_number diff --git a/cla-backend/cryptography-layer.zip b/cla-backend/cryptography-layer.zip new file mode 100644 index 000000000..4c3b3e92a Binary files /dev/null and b/cla-backend/cryptography-layer.zip differ diff --git a/cla-backend/deploy-dev.sh b/cla-backend/deploy-dev.sh index af0b5dac3..a458078a5 100755 --- a/cla-backend/deploy-dev.sh +++ b/cla-backend/deploy-dev.sh @@ -39,4 +39,5 @@ for i in "${golang_files[@]}"; do fi done + yarn deploy:dev diff --git a/cla-backend/helpers/create_signatures.py b/cla-backend/helpers/create_signatures.py index 5967d1e73..347b4222b 100644 --- a/cla-backend/helpers/create_signatures.py +++ b/cla-backend/helpers/create_signatures.py @@ -20,7 +20,10 @@ user = get_user_instance().get_user_by_github_id(USER_GITHUB_ID) project1 = get_project_instance().get_projects_by_external_id(PROJECT_EXTERNAL_ID1)[0] project2 = get_project_instance().get_projects_by_external_id(PROJECT_EXTERNAL_ID2)[0] -company = get_company_instance().get_company_by_external_id(COMPANY_EXTERNAL_ID) +company_list = get_company_instance().get_company_by_external_id(COMPANY_EXTERNAL_ID) +company = None +if company_list: + company = company_list[0] # Test ICLA Agreement. sig_id = str(uuid.uuid4()) diff --git a/cla-backend/package.json b/cla-backend/package.json index 30ea00235..7737152cb 100644 --- a/cla-backend/package.json +++ b/cla-backend/package.json @@ -1,8 +1,11 @@ { - "name": "project", + "name": "easycla-api", "version": "1.0.0", "license": "MIT", "author": "The Linux Foundation", + "engines": { + "node": ">=16" + }, "scripts": { "sls": "./node_modules/serverless/bin/serverless.js", "serve:dev": "./node_modules/serverless/bin/serverless.js wsgi serve -s 'dev' -r us-east-1 --verbose", @@ -32,21 +35,39 @@ "dependencies": { "install": "^0.13.0", "node.extend": "^2.0.2", - "request": "^2.88.0", - "serverless": "^2.19.0", - "serverless-domain-manager": "^5.1.0", - "serverless-finch": "^2.3.2", - "serverless-layers": "^1.4.3", - "serverless-offline": "^6.1.5", + "serverless": "^3.32.2", + "serverless-domain-manager": "^7.0.4", + "serverless-finch": "^4.0.3", + "serverless-layers": "^2.6.1", "serverless-plugin-tracing": "^2.0.0", - "serverless-prune-plugin": "^1.4.2", - "serverless-pseudo-parameters": "^2.5.0", - "serverless-python-requirements": "^4.2.5", - "serverless-wsgi": "^1.5.2" + "serverless-prune-plugin": "^2.0.2", + "serverless-python-requirements": "^6.0.0", + "serverless-wsgi": "^3.0.1", + "xml2js": "^0.6.0", + "yarn-audit-fix": "^10.0.0" }, "resolutions": { - "axios": "^0.21.1", + "ansi-regex": "^5.0.1", + "aws-sdk": "^2.1329.0", + "axios": "^0.28.0", + "cookiejar": "^2.1.4", + "file-type": "^16.5.4", + "glob-parent": "^5.1.2", + "http-cache-semantics": "^4.1.1", "ini": "^1.3.7", - "node-fetch": "^2.6.1" + "jszip": "^3.7.1", + "json-schema": "^0.4.0", + "lodash.set": "^4.3.2", + "node-fetch": "^2.6.7", + "minimatch": "^6.1.6", + "minimist": "^1.2.6", + "normalize-url": "^4.5.1", + "qs": "^6.11.0", + "set-value": "^4.0.1", + "shell-quote": "^1.7.3", + "simple-git": "^3.16.0", + "ws": ">=7.5.10", + "xmlhttprequest-ssl": "^1.6.2", + "fast-xml-parser": ">=4.4.1" } } diff --git a/cla-backend/requirements.txt b/cla-backend/requirements.txt index 342206003..4cebb2536 100644 --- a/cla-backend/requirements.txt +++ b/cla-backend/requirements.txt @@ -1,12 +1,12 @@ # Copyright The Linux Foundation and each contributor to CommunityBridge. # SPDX-License-Identifier: MIT -astroid==2.3.3 +astroid==2.8.0 atomicwrites==1.3.0 attrs==19.3.0 beautifulsoup4==4.8.1 -boto3==1.9.236 -botocore==1.12.253 -certifi==2019.11.28 +boto3==1.22.1 +botocore==1.25.11 +certifi==2023.7.22 chardet==3.0.4 colorama==0.4.3 coverage==4.5.4 @@ -15,48 +15,49 @@ docraptor==1.2.0 docutils==0.15.2 ecdsa==0.14.1 falcon==2.0.0 -future==0.18.2 -furl==2.1.0 +future==0.18.3 gossip==2.3.1 gunicorn==19.9.0 hug==2.6.0 -idna==2.8 +idna==3.7 importlib-metadata==1.6.1 -Jinja2==2.11.2 +Jinja2==3.1.4 jmespath==0.9.4 lazy-object-proxy==1.4.3 Logbook==1.5.3 -lxml==4.6.2 +lxml==4.9.2 more-itertools==8.0.2 nose2==0.9.1 oauthlib==3.1.0 -packaging==19.2 +packaging==20.5 pluggy==0.13.1 -py==1.8.0 +py==1.10.0 pyasn1==0.4.8 pydocusign==2.2 -PyGithub==1.43.8 -PyJWT==1.7.1 -pylint==1.5.2 -pynamodb==3.4.1 +PyGithub==1.55 +PyJWT==2.7.0 +pylint==2.11.1 +pynamodb==4.4.0 pyparsing==2.4.5 pytest==5.0.1 pytest-clarity==0.3.0a0 pytest-cov==2.8.1 -python-dateutil==2.8.0 +python-dateutil==2.8.1 python-jose==3.0.1 -requests==2.22.0 +requests==2.31.0 requests-oauthlib==1.2.0 -rsa==4.0 -s3transfer==0.2.1 +rsa==4.7 +s3transfer==0.5.0 sentinels==1.0.0 six==1.13.0 soupsieve==1.9.5 termcolor==1.1.0 typed-ast==1.4.1 -urllib3==1.25.7 +urllib3==1.26.18 vintage==0.4.1 wcwidth==0.1.7 Werkzeug==0.15.5 wrapt==1.11.2 -zipp==0.6.0 +zipp==3.15.0 +markupsafe==2.0.1 +setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability diff --git a/cla-backend/serverless.yml b/cla-backend/serverless.yml index b44046948..a68429079 100644 --- a/cla-backend/serverless.yml +++ b/cla-backend/serverless.yml @@ -1,48 +1,44 @@ +--- # Copyright The Linux Foundation and each contributor to CommunityBridge. # SPDX-License-Identifier: MIT service: cla-backend -frameworkVersion: '^2.11.0' +frameworkVersion: '^3.28.1' package: # Exclude all first - selectively add in lambda functions - exclude: - - auth/** - - ./backend-aws-lambda - - ./user-subscribe-lambda - - ./metrics-aws-lambda - - ./metrics-report-lambda - - ./dynamo-events-lambda - - ./zipbuilder-scheduler-lambda - - ./zipbuilder-lambda - - ./functional-tests - - dev.sh - - docs/** - - helpers/** - - Makefile - - .env/** - - .venv/** - - .git* - - .git/** - - .vscode/** - - .serverless-wsgi - - .pylintrc - - node_modules/** - - package-lock.json - - yarn.lock + # Support for "package.include" and "package.exclude" will be removed with next major release. Please use "package.patterns" instead + # More Info: https://www.serverless.com/framework/docs/deprecations/#NEW_PACKAGE_PATTERNS + patterns: + - '!auth/**' + - '!bin/*' + - '!dev.sh' + - '!docs/**' + - '!helpers/**' + - '!Makefile' + - '!.env/**' + - '!.venv/**' + - '!.git*' + - '!.git/**' + - '!.vscode/**' + - '!.pylintrc' + - '!node_modules/**' + - '!package-lock.json' + - '!yarn.lock' + - '.serverless-wsgi' custom: - allowed_origins: ${file(./env.json):cla-allowed-origins-${opt:stage}, ssm:/cla-allowed-origins-${opt:stage}} + allowed_origins: ${file(./env.json):cla-allowed-origins-${sls:stage}, ssm:/cla-allowed-origins-${sls:stage}} wsgi: app: cla.routes.__hug_wsgi__ pythonBin: python - packRequirements: false + pythonRequirements: false # Config for serverless-prune-plugin - remove all but the 10 most recent # versions to avoid the "Code storage limit exceeded" error prune: automatic: true number: 3 - userEventsSNSTopicARN: arn:aws:sns:us-east-2:#{AWS::AccountId}:userservice-triggers-${self:provider.stage}-user-sns-topic + userEventsSNSTopicARN: arn:aws:sns:us-east-2:${aws:accountId}:userservice-triggers-${sls:stage}-user-sns-topic certificate: arn: @@ -77,26 +73,26 @@ custom: customDomains: # https://github.com/amplify-education/serverless-domain-manager - primaryDomain: - domainName: ${self:custom.product.domain.name.${opt:stage}, self:custom.product.domain.name.other} - stage: ${opt:stage} + domainName: ${self:custom.product.domain.name.${sls:stage}, self:custom.product.domain.name.other} + stage: ${sls:stage} basePath: '' # a value of '/' will not work securityPolicy: tls_1_2 apiType: rest - certificateArn: ${self:custom.certificate.arn.${opt:stage}, self:custom.certificate.arn.other} + certificateArn: ${self:custom.certificate.arn.${sls:stage}, self:custom.certificate.arn.other} protocols: - https enabled: true - alternateDomain: - domainName: ${self:custom.product.domain.alt.${opt:stage}, self:custom.product.domain.alt.other} - stage: ${opt:stage} + domainName: ${self:custom.product.domain.alt.${sls:stage}, self:custom.product.domain.alt.other} + stage: ${sls:stage} basePath: '' # a value of '/' will not work securityPolicy: tls_1_2 apiType: rest - certificateArn: ${self:custom.certificate.arn.${opt:stage}, self:custom.certificate.arn.other} + certificateArn: ${self:custom.certificate.arn.${sls:stage}, self:custom.certificate.arn.other} protocols: - https - enabled: ${self:custom.product.domain.alt.enabled.${opt:stage}, self:custom.product.domain.alt.enabled.other} + enabled: ${self:custom.product.domain.alt.enabled.${sls:stage}, self:custom.product.domain.alt.enabled.other} ses_from_email: dev: admin@dev.lfcla.com @@ -106,16 +102,17 @@ custom: provider: name: aws runtime: python3.7 - stage: ${opt:stage} + stage: ${env:STAGE} region: us-east-1 timeout: 60 # optional, in seconds, default is 6 logRetentionInDays: 14 + lambdaHashingVersion: '20201221' # Resolution of lambda version hashes was improved with better algorithm, which will be used in next major release. Switch to it now by setting "provider.lambdaHashingVersion" to "20201221" + apiGateway: # https://www.serverless.com/framework/docs/deprecations/#AWS_API_GATEWAY_NAME_STARTING_WITH_SERVICE shouldStartNameWithService: true # Configuring API Gateway to return binary media can be done via the binaryMediaTypes config: binaryMediaTypes: - #- '*/*' - 'image/*' - 'application/pdf' - 'application/zip' @@ -131,204 +128,221 @@ provider: tracing: apiGateway: true - lambda: true - - # Alongside provider.iamRoleStatements managed policies can also be added to this service-wide Role - # These will also be merged into the generated IAM Role - iamManagedPolicies: - - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - - "arn:aws:iam::aws:policy/service-role/AWSLambdaDynamoDBExecutionRole" - - iamRoleStatements: - - Effect: Allow - Action: - - cloudwatch:* - Resource: "*" - - Effect: Allow - Action: - - xray:PutTraceSegments - - xray:PutTelemetryRecords - Resource: "*" - - Effect: Allow - Action: - - s3:GetObject - - s3:PutObject - - s3:DeleteObject - - s3:PutObjectAcl - Resource: - - "arn:aws:s3:::cla-signature-files-${self:provider.stage}/*" - - "arn:aws:s3:::cla-project-logo-${self:provider.stage}/*" - - Effect: Allow - Action: - - s3:ListBucket - Resource: - - "arn:aws:s3:::cla-signature-files-${self:provider.stage}" - - "arn:aws:s3:::cla-project-logo-${self:provider.stage}" - - Effect: Allow - Action: - - lambda:InvokeFunction - Resource: - - "arn:aws:lambda:${self:provider.region}:#{AWS::AccountId}:function:cla-backend-${opt:stage}-zipbuilder-lambda" - - Effect: Allow - Action: - - ssm:GetParameter - Resource: - - "arn:aws:ssm:${self:provider.region}:#{AWS::AccountId}:parameter/cla-*" - - Effect: Allow - Action: - - ses:SendEmail - - ses:SendRawEmail - Resource: - - "*" - Condition: - StringEquals: - ses:FromAddress: ${self:custom.ses_from_email.${opt:stage}} - - Effect: Allow - Action: - - sns:Publish - Resource: - - "*" - - Effect: Allow - Action: - - sqs:SendMessage - Resource: - - "*" - - Effect: Allow - Action: - - dynamodb:Query - - dynamodb:DeleteItem - - dynamodb:UpdateItem - - dynamodb:PutItem - - dynamodb:GetItem - - dynamodb:Scan - - dynamodb:DescribeTable - - dynamodb:BatchGetItem - - dynamodb:GetRecords - - dynamodb:GetShardIterator - - dynamodb:DescribeStream - - dynamodb:ListStreams - Resource: - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-ccla-whitelist-requests" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-cla-manager-requests" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-companies" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-company-invites" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-events" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-gerrit-instances" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-github-orgs" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-projects" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-repositories" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-session-store" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-store" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-user-permissions" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-users" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-metrics" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-projects-cla-groups" - - Effect: Allow - Action: - - dynamodb:Query - Resource: - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-ccla-whitelist-requests/index/company-id-project-id-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-ccla-whitelist-requests/index/ccla-approval-list-request-project-id-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-users/index/github-user-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-users/index/github-username-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-users/index/github-user-external-id-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-users/index/lf-username-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-users/index/lf-email-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-gerrit-instances/index/gerrit-name-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-gerrit-instances/index/gerrit-project-id-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-gerrit-instances/index/gerrit-project-sfid-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/project-signature-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/project-signature-date-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/reference-signature-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/signature-project-reference-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/signature-user-ccla-company-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/project-signature-external-id-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/signature-company-signatory-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/reference-signature-search-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/signature-project-id-type-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/signature-company-initial-manager-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/signature-project-id-sigtype-signed-approved-id-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-companies/index/external-company-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-companies/index/company-name-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-companies/index/company-signing-entity-name-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-projects/index/external-project-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-projects/index/project-name-search-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-projects/index/project-name-lower-search-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-projects/index/foundation-sfid-project-name-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-repositories/index/project-repository-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-repositories/index/repository-name-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-repositories/index/repository-organization-name-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-repositories/index/external-repository-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-repositories/index/sfdc-repository-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-repositories/index/project-sfid-repository-organization-name-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-repositories/index/project-sfid-repository-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-github-orgs/index/github-org-sfid-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-github-orgs/index/project-sfid-organization-name-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-github-orgs/index/organization-name-lower-search-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-company-invites/index/requested-company-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-events/index/event-type-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-events/index/user-id-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-events/index/company-id-external-project-id-event-epoch-time-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-events/index/event-project-id-event-time-epoch-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-events/index/event-date-and-contains-pii-event-time-epoch-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-events/index/company-sfid-foundation-sfid-event-time-epoch-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-events/index/company-sfid-project-id-event-time-epoch-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-events/index/company-id-event-type-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-events/index/event-foundation-sfid-event-time-epoch-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-metrics/index/metric-type-salesforce-id-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-cla-manager-requests/index/cla-manager-requests-company-project-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-cla-manager-requests/index/cla-manager-requests-external-company-project-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-cla-manager-requests/index/cla-manager-requests-project-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-projects-cla-groups/index/cla-group-id-index" - - "arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/cla-${opt:stage}-projects-cla-groups/index/foundation-sfid-index" + lambda: true # optional, enables tracing for all functions (can be true (true equals 'Active') 'Active' or 'PassThrough') + + iam: + role: + # Alongside provider.iam.role.statements managed policies can also be added to this service-wide Role + # These will also be merged into the generated IAM Role + managedPolicies: + - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + - "arn:aws:iam::aws:policy/service-role/AWSLambdaDynamoDBExecutionRole" + statements: + - Effect: Allow + Action: + - cloudwatch:* + Resource: "*" + - Effect: Allow + Action: + - xray:PutTraceSegments + - xray:PutTelemetryRecords + Resource: "*" + - Effect: Allow + Action: + - s3:GetObject + - s3:PutObject + - s3:DeleteObject + - s3:PutObjectAcl + Resource: + - "arn:aws:s3:::cla-signature-files-${sls:stage}/*" + - "arn:aws:s3:::cla-project-logo-${sls:stage}/*" + - Effect: Allow + Action: + - s3:ListBucket + Resource: + - "arn:aws:s3:::cla-signature-files-${sls:stage}" + - "arn:aws:s3:::cla-project-logo-${sls:stage}" + - Effect: Allow + Action: + - lambda:InvokeFunction + Resource: + - "arn:aws:lambda:${self:provider.region}:${aws:accountId}:function:cla-backend-${sls:stage}-zipbuilder-lambda" + - Effect: Allow + Action: + - ssm:GetParameter + Resource: + - "arn:aws:ssm:${self:provider.region}:${aws:accountId}:parameter/cla-*" + - Effect: Allow + Action: + - ses:SendEmail + - ses:SendRawEmail + Resource: + - "*" + Condition: + StringEquals: + ses:FromAddress: ${self:custom.ses_from_email.${sls:stage}} + - Effect: Allow + Action: + - sns:Publish + Resource: + - "*" + - Effect: Allow + Action: + - sqs:SendMessage + Resource: + - "*" + - Effect: Allow + Action: + - dynamodb:Query + - dynamodb:DeleteItem + - dynamodb:UpdateItem + - dynamodb:PutItem + - dynamodb:GetItem + - dynamodb:Scan + - dynamodb:DescribeTable + - dynamodb:BatchGetItem + - dynamodb:GetRecords + - dynamodb:GetShardIterator + - dynamodb:DescribeStream + - dynamodb:ListStreams + Resource: + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-ccla-whitelist-requests" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-cla-manager-requests" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-companies" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-company-invites" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-events" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-gerrit-instances" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-github-orgs" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-projects" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-repositories" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-session-store" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-signatures" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-store" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-user-permissions" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-users" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-metrics" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-projects-cla-groups" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-gitlab-orgs" + + - Effect: Allow + Action: + - dynamodb:Query + Resource: + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-ccla-whitelist-requests/index/company-id-project-id-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-ccla-whitelist-requests/index/ccla-approval-list-request-project-id-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-users/index/github-id-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-users/index/github-username-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-users/index/gitlab-id-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-users/index/gitlab-username-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-users/index/github-user-external-id-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-users/index/lf-username-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-users/index/lf-email-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-gerrit-instances/index/gerrit-name-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-gerrit-instances/index/gerrit-project-id-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-gerrit-instances/index/gerrit-project-sfid-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-signatures/index/project-signature-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-signatures/index/project-signature-date-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-signatures/index/reference-signature-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-signatures/index/signature-project-reference-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-signatures/index/signature-user-ccla-company-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-signatures/index/project-signature-external-id-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-signatures/index/signature-company-signatory-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-signatures/index/reference-signature-search-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-signatures/index/signature-project-id-type-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-signatures/index/signature-company-initial-manager-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-signatures/index/signature-project-id-sigtype-signed-approved-id-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-companies/index/external-company-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-companies/index/company-name-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-companies/index/company-signing-entity-name-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-projects/index/external-project-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-projects/index/project-name-search-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-projects/index/project-name-lower-search-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-projects/index/foundation-sfid-project-name-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-repositories/index/project-repository-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-repositories/index/repository-name-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-repositories/index/repository-organization-name-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-repositories/index/external-repository-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-repositories/index/sfdc-repository-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-repositories/index/project-sfid-repository-organization-name-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-repositories/index/project-sfid-repository-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-repositories/index/repository-type-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-github-orgs/index/github-org-sfid-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-github-orgs/index/project-sfid-organization-name-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-github-orgs/index/organization-name-lower-search-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-company-invites/index/requested-company-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-events/index/event-type-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-events/index/user-id-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-events/index/company-id-external-project-id-event-epoch-time-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-events/index/event-project-id-event-time-epoch-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-events/index/event-cla-group-id-event-time-epoch-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-events/index/event-date-and-contains-pii-event-time-epoch-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-events/index/company-sfid-foundation-sfid-event-time-epoch-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-events/index/company-sfid-project-id-event-time-epoch-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-events/index/company-id-event-type-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-events/index/event-foundation-sfid-event-time-epoch-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-events/index/event-company-sfid-event-data-lower-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-events/index/company-sfid-cla-group-id-event-time-epoch-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-metrics/index/metric-type-salesforce-id-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-cla-manager-requests/index/cla-manager-requests-company-project-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-cla-manager-requests/index/cla-manager-requests-external-company-project-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-cla-manager-requests/index/cla-manager-requests-project-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-projects-cla-groups/index/cla-group-id-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-projects-cla-groups/index/foundation-sfid-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-gitlab-orgs/index/gitlab-org-sfid-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-gitlab-orgs/index/gitlab-project-sfid-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-gitlab-orgs/index/gitlab-organization-name-lower-search-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-gitlab-orgs/index/gitlab-project-sfid-organization-name-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-gitlab-orgs/index/gitlab-full-path-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-gitlab-orgs/index/gitlab-external-group-id-index" + - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/cla-${sls:stage}-gitlab-orgs/index/gitlab-org-url-index" environment: - STAGE: ${self:provider.stage} + STAGE: ${sls:stage} HOME: /tmp REGION: us-east-1 DYNAMODB_AWS_REGION: us-east-1 - GH_APP_WEBHOOK_SECRET: ${file(./env.json):gh-app-webhook-secret, ssm:/cla-gh-app-webhook-secret-${opt:stage}~true} - GH_APP_ID: ${file(./env.json):gh-app-id, ssm:/cla-gh-app-id-${opt:stage}~true} - GH_OAUTH_CLIENT_ID: ${file(./env.json):gh-oauth-client-id, ssm:/cla-gh-oauth-client-id-${opt:stage}~true} - GH_OAUTH_SECRET: ${file(./env.json):gh-oauth-secret, ssm:/cla-gh-oauth-secret-${opt:stage}~true} - GITHUB_OAUTH_TOKEN: ${file(./env.json):gh-access-token, ssm:/cla-gh-access-token-${opt:stage}~true} - GITHUB_APP_WEBHOOK_SECRET: ${file(./env.json):gh-app-webhook-secret, ssm:/cla-gh-app-webhook-secret-${opt:stage}~true} + GH_APP_WEBHOOK_SECRET: ${file(./env.json):gh-app-webhook-secret, ssm:/cla-gh-app-webhook-secret-${sls:stage}} + GH_APP_ID: ${file(./env.json):gh-app-id, ssm:/cla-gh-app-id-${sls:stage}} + GH_OAUTH_CLIENT_ID: ${file(./env.json):gh-oauth-client-id, ssm:/cla-gh-oauth-client-id-${sls:stage}} + GH_OAUTH_SECRET: ${file(./env.json):gh-oauth-secret, ssm:/cla-gh-oauth-secret-${sls:stage}} + GITHUB_OAUTH_TOKEN: ${file(./env.json):gh-access-token, ssm:/cla-gh-access-token-${sls:stage}} + GITHUB_APP_WEBHOOK_SECRET: ${file(./env.json):gh-app-webhook-secret, ssm:/cla-gh-app-webhook-secret-${sls:stage}} GH_STATUS_CTX_NAME: "EasyCLA" - AUTH0_DOMAIN: ${file(./env.json):auth0-domain, ssm:/cla-auth0-domain-${opt:stage}~true} - AUTH0_CLIENT_ID: ${file(./env.json):auth0-clientId, ssm:/cla-auth0-clientId-${opt:stage}~true} - AUTH0_USERNAME_CLAIM: ${file(./env.json):auth0-username-claim, ssm:/cla-auth0-username-claim-${opt:stage}} - AUTH0_ALGORITHM: ${file(./env.json):auth0-algorithm, ssm:/cla-auth0-algorithm-${opt:stage}} - SF_INSTANCE_URL: ${file(./env.json):sf-instance-url, ssm:/cla-sf-instance-url-${opt:stage}~true} - SF_CLIENT_ID: ${file(./env.json):sf-client-id, ssm:/cla-sf-consumer-key-${opt:stage}~true} - SF_CLIENT_SECRET: ${file(./env.json):sf-client-secret, ssm:/cla-sf-consumer-secret-${opt:stage}~true} - SF_USERNAME: ${file(./env.json):sf-username, ssm:/cla-sf-username-${opt:stage}~true} - SF_PASSWORD: ${file(./env.json):sf-password, ssm:/cla-sf-password-${opt:stage}~true} - DOCRAPTOR_API_KEY: ${file(./env.json):doc-raptor-api-key, ssm:/cla-doc-raptor-api-key-${opt: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}} - CLA_CONTRIBUTOR_V2_BASE: ${file(./env.json):cla-contributor-v2-base, ssm:/cla-contributor-v2-base-${opt:stage}} - CLA_CORPORATE_BASE: ${file(./env.json):cla-corporate-base, ssm:/cla-corporate-base-${opt:stage}} - CLA_LANDING_PAGE: ${file(./env.json):cla-landing-page, ssm:/cla-landing-page-${opt:stage}} - CLA_SIGNATURE_FILES_BUCKET: ${file(./env.json):cla-signature-files-bucket, ssm:/cla-signature-files-bucket-${opt:stage}~true} - CLA_BUCKET_LOGO_URL: ${file(./env.json):cla-logo-url, ssm:/cla-logo-url-${opt:stage}~true} - SES_SENDER_EMAIL_ADDRESS: ${file(./env.json):cla-ses-sender-email-address, ssm:/cla-ses-sender-email-address-${opt:stage}} - SMTP_SENDER_EMAIL_ADDRESS: ${file(./env.json):cla-smtp-sender-email-address, ssm:/cla-smtp-sender-email-address-${opt:stage}} - LF_GROUP_CLIENT_ID: ${file(./env.json):lf-group-client-id, ssm:/cla-lf-group-client-id-${opt:stage}} - LF_GROUP_CLIENT_SECRET: ${file(./env.json):lf-group-client-secret, ssm:/cla-lf-group-client-secret-${opt:stage}} - LF_GROUP_REFRESH_TOKEN: ${file(./env.json):lf-group-refresh-token, ssm:/cla-lf-group-refresh-token-${opt:stage}} - LF_GROUP_CLIENT_URL: ${file(./env.json):lf-group-client-url, ssm:/cla-lf-group-client-url-${opt:stage}} - SNS_EVENT_TOPIC_ARN: ${file(./env.json):sns-event-topic-arn, ssm:/cla-sns-event-topic-arn-${opt:stage}} - PLATFORM_AUTH0_URL: ${file(./env.json):cla-auth0-platform-url, ssm:/cla-auth0-platform-url-${opt:stage}} - PLATFORM_AUTH0_CLIENT_ID: ${file(./env.json):cla-auth0-platform-client-id, ssm:/cla-auth0-platform-client-id-${opt:stage}} - PLATFORM_AUTH0_CLIENT_SECRET: ${file(./env.json):cla-auth0-platform-client-secret, ssm:/cla-auth0-platform-client-secret-${opt:stage}} - PLATFORM_AUTH0_AUDIENCE: ${file(./env.json):cla-auth0-platform-audience, ssm:/cla-auth0-platform-audience-${opt:stage}} - PLATFORM_GATEWAY_URL: ${file(./env.json):platform-gateway-url, ssm:/cla-auth0-platform-api-gw-${opt:stage}} - PLATFORM_MAINTAINERS: ${file(./env.json):platform-maintainers, ssm:/cla-lf-platform-maintainers-${opt:stage}} + AUTH0_DOMAIN: ${file(./env.json):auth0-domain, ssm:/cla-auth0-domain-${sls:stage}} + AUTH0_CLIENT_ID: ${file(./env.json):auth0-clientId, ssm:/cla-auth0-clientId-${sls:stage}} + AUTH0_USERNAME_CLAIM: ${file(./env.json):auth0-username-claim, ssm:/cla-auth0-username-claim-${sls:stage}} + AUTH0_ALGORITHM: ${file(./env.json):auth0-algorithm, ssm:/cla-auth0-algorithm-${sls:stage}} + SF_INSTANCE_URL: ${file(./env.json):sf-instance-url, ssm:/cla-sf-instance-url-${sls:stage}} + SF_CLIENT_ID: ${file(./env.json):sf-client-id, ssm:/cla-sf-consumer-key-${sls:stage}} + SF_CLIENT_SECRET: ${file(./env.json):sf-client-secret, ssm:/cla-sf-consumer-secret-${sls:stage}} + 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_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_AUTH_SERVER: ${file(./env.json):docusign-auth-server, ssm:/cla-docusign-auth-server-${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}} + CLA_CONTRIBUTOR_V2_BASE: ${file(./env.json):cla-contributor-v2-base, ssm:/cla-contributor-v2-base-${sls:stage}} + CLA_CORPORATE_BASE: ${file(./env.json):cla-corporate-base, ssm:/cla-corporate-base-${sls:stage}} + CLA_CORPORATE_V2_BASE: ${file(./env.json):cla-corporate-v2-base, ssm:/cla-corporate-v2-base-${sls:stage}} + CLA_LANDING_PAGE: ${file(./env.json):cla-landing-page, ssm:/cla-landing-page-${sls:stage}} + CLA_SIGNATURE_FILES_BUCKET: ${file(./env.json):cla-signature-files-bucket, ssm:/cla-signature-files-bucket-${sls:stage}} + CLA_BUCKET_LOGO_URL: ${file(./env.json):cla-logo-url, ssm:/cla-logo-url-${sls:stage}} + SES_SENDER_EMAIL_ADDRESS: ${file(./env.json):cla-ses-sender-email-address, ssm:/cla-ses-sender-email-address-${sls:stage}} + SMTP_SENDER_EMAIL_ADDRESS: ${file(./env.json):cla-smtp-sender-email-address, ssm:/cla-smtp-sender-email-address-${sls:stage}} + LF_GROUP_CLIENT_ID: ${file(./env.json):lf-group-client-id, ssm:/cla-lf-group-client-id-${sls:stage}} + LF_GROUP_CLIENT_SECRET: ${file(./env.json):lf-group-client-secret, ssm:/cla-lf-group-client-secret-${sls:stage}} + LF_GROUP_REFRESH_TOKEN: ${file(./env.json):lf-group-refresh-token, ssm:/cla-lf-group-refresh-token-${sls:stage}} + LF_GROUP_CLIENT_URL: ${file(./env.json):lf-group-client-url, ssm:/cla-lf-group-client-url-${sls:stage}} + SNS_EVENT_TOPIC_ARN: ${file(./env.json):sns-event-topic-arn, ssm:/cla-sns-event-topic-arn-${sls:stage}} + PLATFORM_AUTH0_URL: ${file(./env.json):cla-auth0-platform-url, ssm:/cla-auth0-platform-url-${sls:stage}} + PLATFORM_AUTH0_CLIENT_ID: ${file(./env.json):cla-auth0-platform-client-id, ssm:/cla-auth0-platform-client-id-${sls:stage}} + PLATFORM_AUTH0_CLIENT_SECRET: ${file(./env.json):cla-auth0-platform-client-secret, ssm:/cla-auth0-platform-client-secret-${sls:stage}} + PLATFORM_AUTH0_AUDIENCE: ${file(./env.json):cla-auth0-platform-audience, ssm:/cla-auth0-platform-audience-${sls:stage}} + PLATFORM_GATEWAY_URL: ${file(./env.json):platform-gateway-url, ssm:/cla-auth0-platform-api-gw-${sls:stage}} + PLATFORM_MAINTAINERS: ${file(./env.json):platform-maintainers, ssm:/cla-lf-platform-maintainers-${sls:stage}} # Set to true for verbose API logging - useful when Debugging API calls for Core Platform Services or other external services # LOG_DEVEL: debug # default is debug # DEBUG: false # default is false @@ -343,22 +357,22 @@ provider: stackTags: Name: ${self:service} - stage: ${self:provider.stage} + stage: ${sls:stage} Project: "EasyCLA" Product: "EasyCLA" ManagedBy: "Serverless CloudFormation" - SericeType: "Product" + ServiceType: "Product" Service: ${self:service} ServiceRole: "Backend" ProgrammingPlatform: Go Owner: "David Deal" tags: Name: ${self:service} - stage: ${self:provider.stage} + stage: ${sls:stage} Project: "EasyCLA" Product: "EasyCLA" ManagedBy: "Serverless CloudFormation" - SericeType: "Product" + ServiceType: "Product" Service: ${self:service} ServiceRole: "Backend" ProgrammingPlatform: Go @@ -368,14 +382,12 @@ plugins: - serverless-python-requirements - serverless-wsgi - serverless-plugin-tracing - - serverless-pseudo-parameters # Serverless Finch does s3 uploading. Called with 'sls client deploy'. # Also allows bucket removal with 'sls client remove'. - serverless-finch # To avoid a Code Storage Limit after tons of deploys and revisions - we can prune old versions # This plugin allows us to remove/prune the old versions either manually or automatically - serverless-prune-plugin - - serverless-offline - serverless-domain-manager functions: @@ -385,13 +397,14 @@ functions: runtime: go1.x package: individually: true - include: - - auth/bin/** + patterns: + - 'auth/bin/**' - apiv3: - runtime: go1.x - handler: backend-aws-lambda + api-v3-lambda: + name: ${self:service}-${sls:stage, 'dev'}-api-v3-lambda description: "EasyCLA Golang API handler for the /v3 endpoints" + runtime: go1.x + handler: 'bin/backend-aws-lambda' events: - http: method: ANY @@ -399,73 +412,81 @@ functions: # cors: true # CORS handled at the API implementation package: individually: true - include: - - ./backend-aws-lambda + patterns: + - '!**' + - 'bin/backend-aws-lambda' dynamo-projects-events-lambda: - handler: dynamo-events-lambda - name: ${self:service}-${opt:stage, self:provider.stage, 'dev'}-dynamo-projects-lambda + name: ${self:service}-${sls:stage, 'dev'}-dynamo-projects-lambda description: "EasyCLA DynamoDB stream events handler for the projects table" + handler: 'bin/dynamo-events-lambda' runtime: go1.x package: individually: true - include: - - ./dynamo-events-lambda + patterns: + - '!**' + - 'bin/dynamo-events-lambda' dynamo-signatures-events-lambda: - handler: dynamo-events-lambda - name: ${self:service}-${opt:stage, self:provider.stage, 'dev'}-dynamo-signatures-events-lambda + handler: 'bin/dynamo-events-lambda' + name: ${self:service}-${sls:stage, 'dev'}-dynamo-signatures-events-lambda description: "EasyCLA DynamoDB stream events handler for the signatures table" runtime: go1.x package: individually: true - include: - - ./dynamo-events-lambda + patterns: + - '!**' + - 'bin/dynamo-events-lambda' dynamo-events-events-lambda: - handler: dynamo-events-lambda - name: ${self:service}-${opt:stage, self:provider.stage, 'dev'}-dynamo-events-events-lambda + handler: 'bin/dynamo-events-lambda' + name: ${self:service}-${sls:stage, 'dev'}-dynamo-events-events-lambda description: "EasyCLA DynamoDB stream events handler for the events table" runtime: go1.x package: individually: true - include: - - ./dynamo-events-lambda + patterns: + - '!**' + - 'bin/dynamo-events-lambda' dynamo-repositories-events-lambda: - handler: dynamo-events-lambda - name: ${self:service}-${opt:stage, self:provider.stage, 'dev'}-dynamo-repositories-events-lambda + handler: 'bin/dynamo-events-lambda' + name: ${self:service}-${sls:stage, 'dev'}-dynamo-repositories-events-lambda description: "EasyCLA DynamoDB stream events handler for the repositories table" runtime: go1.x package: individually: true - include: - - ./dynamo-events-lambda + patterns: + - '!**' + - 'bin/dynamo-events-lambda' dynamo-projects-cla-groups-events-lambda: - handler: dynamo-events-lambda - name: ${self:service}-${opt:stage, self:provider.stage, 'dev'}-dynamo-projects-cla-groups-events-lambda + handler: 'bin/dynamo-events-lambda' + name: ${self:service}-${sls:stage, 'dev'}-dynamo-projects-cla-groups-events-lambda description: "EasyCLA DynamoDB stream events handler for the projects-cla-groups table" runtime: go1.x package: individually: true - include: - - ./dynamo-events-lambda + patterns: + - '!**' + - 'bin/dynamo-events-lambda' dynamo-github-orgs-events-lambda: - handler: dynamo-events-lambda - name: ${self:service}-${opt:stage, self:provider.stage, 'dev'}-dynamo-github-orgs-events-lambda + handler: 'bin/dynamo-events-lambda' + name: ${self:service}-${sls:stage, 'dev'}-dynamo-github-orgs-events-lambda description: "EasyCLA DynamoDB stream events handler for cla-
-
- Can you manage CLAs on behalf of your company? Are you authorized to approve the people who contribute on behalf - of your company? -
- - - -- Directions on who signs this and what happens. - The link will direct user to CLA corporate console. -
-Determining Your Next Step
-{{ project.project_name }} requires contributors covered by a corporate CLA to also sign an individual CLA. Click the button below to sign an individual CLA.
- - -You've completed the CLA steps necessary to contribute. You can now return to writing awesome stuff.
-
- If you had a pull request in process you may need to refresh the page to see the updated checks.
-- If you were logged in to your Gerrit Instance, you may need to log out and sign-in again.
- -Unfortunately, you are not yet approved by the Corporate CLA of {{ company.company_name }} - on {{ project.project_name }}.
-- {{ company.company_name }} has whitelisted the following GitHub Organization(s), and all public - members therein will be approved to contribute: -
-- - If you are currently a member of an Org listed above, but haven't publicized it, please - - change your GitHub settings to make this public, and then come back to EasyCLA and select - "Corporate" > {{ company.company_name }} again. You should be all set! -
- -- If you are not a member of an Org listed above, please ask your GitHub administrator to add you (and - then set your membership to public). -
- -- Make sure your employee email is verified in your GitHub account settings. -
- -Go to GitHub and make sure your employee email address is associated with your GitHub account. Then - restart this process from the PR status message. Feel free to close this page. -
- - - -- Ask your CLA Manager to approved your GitHub Username, rather than Email when you contact them - below. -
-- Try making your GitHub email address public. -
-You must be authorized under a signed Contributor License Agreement. You are contributing on behalf - of your work for a company. Contact your CLA manager to request authorization.
- -| {{ company.company_name }} | -
- We are generating a document to sign for the purposes of your CLA. Please fill out - accurately and complete the signing process. -
-- If a new tab with the document to sign did not open, you may use - the button below: -
- -- Whoops, It looks like you don't have any signatures in progress. Try logging in with your Linux - Foundation ID again. -
-- We are generating a document to sign for the purposes of your CLA. Please fill out - accurately and complete the signing process. -
-- If a new tab with the document to sign did not open, you may use - the button below: -
- -- The Individual CLA template has not been selected by the Project Manager. Please create a ticket to help us resolve this issue. -
-- It looks like something has gone wrong. Please create a ticket to - help - us resolve this issue. -
-- Whoops, It looks like you don't have any signatures in progress. Try going back to your pull request - and - restarting the signing process from your pull request if necessary. -
-Select this if you are contributing code as an employee.
-Select this if you are contributing code as an individual.
-
-
-